一。并发&并行
一个应用程序 ---> 一个进程 ---> 运行在自己内存地址空间里的独立执行体 ---> 同一个内存地址空间的一起工作的多个线程
一个并发程序 ---> 多个线程来执行任务 ---> 某个时间点同时运行在多核或者多处理器 ---> 并发&并行
---> 某个时间点同时运行在单个处理器 -----> 并发&不并行
并行是一种通过使用多处理器以提高速度的能力。所以并发程序可以是并行的,也可以不是。
公认的,使用多线程的应用难以做到准确,最主要的问题是内存中的数据共享,它们会被多线程以无法预知的方式进行操作,导致一些无法重现或者随机的结果(称作 竞态
)
并发方式:
1.确定性的(明确定义排序)
2.非确定性的(加锁/互斥从而未定义排序)---> 竞态
不要使用全局变量或者共享内存,它们会给你的代码在并发运算的时候带来危险。
解决之道:
1.同步不同的线程,对数据加锁,这样同时就只有一个线程可以变更数据
2.有个被称作 Communicating Sequential Processes(顺序通信处理)
(CSP, C. Hoare 发明的)
3.还有一个叫做 message passing-model(消息传递)
(已经运用在了其他语言中,比如 Erlang)
二。go的协程
在 Go 中,应用程序并发处理的部分被称作 goroutines(协程)
协程 ---> 工作在相同的地址空间 ---> 共享内存的方式一定是同步的;这个可以使用 sync
包来实现(不推荐)
---> 使用 channels
来同步协程
特点:
1.使用少量的内存和资源:使用 4K 的栈内存就可以在堆中创建它们
2.对栈进行了分割,从而动态的增加(或缩减)内存的使用;栈的管理是自动的,但不是由垃圾回收器管理的,而是在协程退出后自动释放
3.协程可以运行在多个操作系统线程之间,也可以运行在线程之内
4.使用少量的操作系统线程就能拥有任意多个提供服务的协程,而且 Go 运行时可以聪明的意识到哪些协程被阻塞了,暂时搁置它们并处理其他协程
5.存在两种并发方式:确定性的(明确定义排序)和非确定性的(加锁/互斥从而未定义排序)。Go 的协程和通道理所当然的支持确定性的并发方式(例如通道具有一个 sender 和一个 receiver)
实现形式:
关键字 go
调用 ---> 一个函数或者方法 ---> 在当前的计算过程中开始一个同时进行的函数 ---> 在相同的地址空间中并且分配了独立的栈(栈分割)
协程的栈会根据需要进行伸缩,不出现栈溢出;开发者不需要关心栈的大小。当协程结束的时候,它会静默退出:用来启动这个协程的函数不会得到任何的返回值
三。go协程并行:
Go 默认没有并行指令,只有一个独立的核心或处理器被专门用于 Go 程序,不论它启动了多少个协程;所以这些协程是并发运行的,但他们不是并行运行的:同一时间只有一个协程会处在运行状态
在 gc 编译器下(6g 或者 8g)你必须设置 GOMAXPROCS 为一个大于默认值 1 的数值来允许运行时支持使用多于 1 个的操作系统线程,所有的协程都会共享同一个线程除非将 GOMAXPROCS 设置为一个大于 1 的数。当 GOMAXPROCS 大于 1 时,会有一个线程池管理许多的线程。通过 gccgo
编译器 GOMAXPROCS 有效的与运行中的协程数量相等。假设 n 是机器上处理器或者核心的数量。如果你设置环境变量 GOMAXPROCS>=n,或者执行 runtime.GOMAXPROCS(n)
,接下来协程会被分割(分散)到 n 个处理器上。更多的处理器并不意味着性能的线性提升。有这样一个经验法则,对于 n 个核心的情况设置 GOMAXPROCS 为 n-1 以获得最佳性能,也同样需要遵守这条规则:协程的数量 > 1 + GOMAXPROCS > 1。
所以如果在某一时间只有一个协程在执行,不要设置 GOMAXPROCS!
还有一些通过实验观察到的现象:在一台 1 颗 CPU 的笔记本电脑上,增加 GOMAXPROCS 到 9 会带来性能提升。在一台 32 核的机器上,设置 GOMAXPROCS=8 会达到最好的性能,在测试环境中,更高的数值无法提升性能。如果设置一个很大的 GOMAXPROCS 只会带来轻微的性能下降;设置 GOMAXPROCS=100,使用 top
命令和 H
选项查看到只有 7 个活动的线程。
增加 GOMAXPROCS 的数值对程序进行并发计算是有好处的;
总结:GOMAXPROCS 等同于(并发的)线程数量,在一台核心数多于1个的机器上,会尽可能有等同于核心数的线程在并行运行。