Files
the-way-to-go_ZH_CN/eBook/14.1.md
2015-12-24 10:24:21 +08:00

7.7 KiB
Raw Blame History

14.1 并发,并行和协程

14.1.1 什么是协程

一个应用程序是运行在机器上的一个进程;进程是一个运行在自己内存地址空间里的独立执行体。一个进程由一个或多个操作系统线程组成,这些线程其实是共享同一个内存地址空间的一起工作的执行体。几乎所有'正式'的程序都是多线程的以便让用户或计算机不必等待或者能够同时服务多个请求如Web服务器或增加性能和吞吐量例如通过对不同的数据集并行执行代码。一个并发程序可以在一个处理器或者内核上使用多个线程来执行任务但是只有在同一个程序在某一个时间点在多个些处理内核或处理器上同时执行的任务才是真正的并行。

并行是一种通过使用多处理器以提高速度的能力。所以并发程序可以是并行的,也可以不是。

公认的,使用多线程的应用难以做到准确,最主要的问题是内存中的数据共享,它们会被多线程以无法预知的方式进行操作,导致一些无法重现或者随机的结果(称作竞态

!!不要使用全局变量或者共享内存,它们会给你的代码在并发运算的时候带来危险。

解决之道在于同步不同的线程对数据加锁这样同时就只有一个线程可以变更数据。在Go的标准库sync中有一些工具用来在低级别的代码中实现加锁;我们在章节9.3中讨论过这个问题。不过过去的软件开发经验告诉我们这会带来更高的复杂度,更容易使代码出错以及更低的性能,所以这个经典的方法明显不再适合现代多核/多处理器编程:"thread-per-connection"模型不够有效。

Go更倾向于其他的方式在诸多比较合适的范式中有个被称作Communicating Sequential Processes顺序通信处理(CSP, C. Hoare发明的)还有一个叫做message passing-model消息传递已经运用在了其他语言中比如Eralng

在Go中应用程序并发处理的部分被称作goroutines协程它可以进行更有效的并发运算。在协程和操作系统线程之间并无一对一的关系协程是根据一个或多个线程的可用性映射多路复用执行于在他们之上的协程调度器在Go运行时很好的完成了这个工作。

协程工作在相同的地址空间中,所以共享内存的方式一定是同步的;这个可以使用sync包来实现(参见章节9.3)不过我们很不鼓励这样做Go使用channels来同步协程(可以参见14.2等章节)

当系统调用比如等待I/O阻塞协程时其他协程会继续在其他线程上工作。协程的设计隐藏了许多线程创建和管理方面的复杂工作。

协程是轻量的比线程更轻。它们痕迹非常不明显使用少量的内存和资源使用4K的栈内存就可以在堆中创建它们。因为创建非常廉价必要的时候可以轻松创建并运行大量的协程在同一个一个地址空间中100,000个连续的协程。并且它们对栈进行了分割从而动态的增加或缩减内存的使用栈的管理是自动的但不是由垃圾回收器管理的而是在协程退出后自动释放。

协程可以运行在多个操作系统线程之间也可以运行在线程之内让你可以很小的内存占用就可以处理大量的任务。由于操作系统线程上的协程时间片你可以使用少量的操作系统线程就能拥有任意多个提供服务的协程而且Go运行时可以聪明的意识到哪些协程被阻塞了暂时搁置它们并处理其他协程。

存在两种并发方式:确定性的(明确定义排序)和非确定性的(加锁/互斥从而未定义排序。Go的协程和通道理所当然的支持确定性的并发方式例如通道具有一个sender和一个receiver。我们会在章节14.7中使用一个常见的算法问题(工人问题)来对比两种处理方式。

协程是通过使用关键字go调用执行一个函数或者方法来实现的也可以是匿名或者lambda函数。这样会在当前的计算过程中开始一个同时进行的函数在相同的地址空间中并且分配了独立的栈比如go sum(bigArray)// 在后台计算总和

协程的栈会根据需要进行伸缩,不出现栈溢出;开发者不需要关心栈的大小。当协程结束的时候,它会静默退出:用来启动这个协程的函数不会得到任何的返回值。

任何Go程序都必须有的main()函数也可以看做是一个协程,尽管它并没有通过go来启动。协程可以在程序初始化的过程中运行(在init()函数中)。

在一个协程中,比如它需要进行非常密集的运算,你可以在运算循环中周期的使用runtime.Gosched():这会让出处理器,允许运行其他协程;它并不会使当前协程挂起,所以它会自动恢复执行。使用Gosched()可以使计算均匀分布,使通信不至于迟迟得不到响应。

14.1.2 并发和并行的差异

Go的并发原语提供了良好的并发设计基础表达程序结构以便表示独立地执行的动作所以Go的的重点不在于并行的首要位置并发程序可能是并行的也可能不是。并行是一种通过使用多处理器以提高速度的能力。但往往是一个设计良好的并发程序在并行方面的表现也非常出色。

在当前的运行时2012年一月实现中Go默认没有并行指令只有一个独立的核心或处理器被专门用于Go程序不论它启动了多少个协程所以这些协程是并发运行的但他们不是并行运行的同一时间只有一个协程会处在运行状态。

这个情况在以后可能会发生改变,不过届时,为了使你的程序可以使用多个核心运行,这时协程就真正的是并行运行了,你必须使用GOMAXPROCS变量。

这会告诉运行时有多少个协程同时执行。

并且只有gc编译器真正实现了协程适当的把协程映射到操作系统线程。使用gccgo编译器,会为每一个协程创建操作系统线程。

14.1.3 使用GOMAXPROCS

在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个活动的线程。

链接