2014年2月23日11:09
Go
如果你刚接触 Go,或者你不理解“并发性不是并行性”这句话,那么可以看下 Rob Pike 关于这个主题的精彩演讲。大概有30分钟,我保证看它30分钟是值得的。
总结一下 —— “当人们听到并发这个词时,他们经常想到并行性,一个相关但非常不同的概念。在编程中,并发性是由独立执行的进程组成的,而并行性是同时执行(可能相关的)计算。并发是指同时处理很多事情。并行是指同时做很多事情。” [1]
Go 允许我们编写并发程序。它提供了goroutines,更重要的是 goroutines 之间的通信功能。下面我讲一下 goroutines。
Goroutines 和 线程 的区别
Go 使用 goroutines,而 Java 这样的语言使用线程。两者有什么区别?我们需要看 3 个因素-内存消耗,创建和销毁成本和切换成本。
内存消耗
创建 goroutine 不需要太多内存,只需要 2kB 的栈空间。它们通过堆的按需分配和释放来增长栈空间。[2][3]另一方面,线程的起始内存为 1Mb(是 goroutine 的 500 倍以上),同时还有一个称为保护页(guard page)的内存区域,该区域在一个线程的内存和另一个线程的内存之间起到保护作用。
因此,一个处理传入请求的服务器可以毫无问题地为每个请求创建一个 goroutine,但每为每个请求创建一个线程最终将导致可怕的 OutOfMemoryError。这并不局限于 Java — 任何使用操作系统线程作为主要并发手段的语言都会面临这个问题。
创建和销毁成本
线程的创建和销毁成本开销非常大,因为它必须从操作系统请求资源并在用完后释放。解决这个问题的方法是维护一个线程池。相反,goroutine 是由运行环境(runtime)创建和销毁的,这些操作的开销很小。Go 语言不支持 goroutine 的手动管理。
切换成本
当一个线程阻塞时,另一个线程需要被调度到当前处理器上运行。线程是抢占式(preemptively)的,在线程切换到另一个线程时,调度器需要保存/恢复所有寄存器,即 16 个通用寄存器,程序指针(program counter),栈指针(stack pointer),段寄存器(segment registers)和16个 XMM 寄存器,浮点协处理器状态,16个 AVX 寄存器,所有的特殊模块寄存器(MSR)等。当在线程间快速切换时,成本可就大了。
Goroutines 是协作式(cooperatively)的,当发生切换时,只需要保存/恢复 3 个寄存器-程序指针、栈指针和 DX。成本要低得多。
正如前面所讨论的,goroutine 的数量通常要比线程高得多,但是由于两个原因,一是只考虑可运行的 goroutine,不考虑阻塞的 goroutine。二是现代的调度器的复杂度为O(1),这意味着切换时间不受选择(线程或 goroutine)数量的影响。所以 goroutine 的数量对切换时间没有影响。[5]
如何执行 goroutine
如前所述,从创建到调度再到销毁,运行环境始终管理 goroutine。运行环境分配了几个线程,所有 goroutine 都在这些线程上进行多路复用。在任何时候,每个线程都将执行一个 goroutine。如果该 goroutine 被阻塞,那么会有另外一个 goroutine 在该线程上执行。[6]
由于 goroutine 是协作式的,一个不停循环的 goroutine 可能会耗尽同一线程上的其他 goroutine。在 Go 1.2 中,这个问题的解决办法是通过在调用一个函数时偶尔出发 Go 的调度器,当一个循环里没有被内联函数时,就可以被预先占用啦。
Goroutines 阻塞
goroutine 开销很小,并且在下面的阻塞情况下,也不会导致运行的线程阻塞。
- 网络输入
- 睡觉
- channels 操作
- sync包中会阻塞的基本操作
即使已经创建了数以万计的 goroutine,并大多数 goroutine 都被阻塞在其中一个 goroutine 上,也不会浪费太多系统资源,因为运行环境会调度另一个 goroutine。
简单来说,goroutines 是线程上的轻量级抽象。Go 程序员不处理线程,类似地,操作系统也不知道 goroutine 的存在。从操作系统的角度来看,Go 程序的行为类似于事件驱动的 C 程序。[5]
线程和处理器
尽管不能直接控制运行环境将创建的线程数量,但可以设置程序使用的处理器内核数。通过设置变量 GOMAXPROCS 并调用 runtime.GOMAXPROCS(n)。增加处理器内核的数量不一定能提高程序的性能,这取决于程序本身的设计。分析工具(profiling)可以用来为你的程序找到理想的内核数。
结束语
与其他语言一样,重要的是防止多个 goroutine 同时访问共享资源。最好使用通道(channel)在 goroutine 之间传输数据,即 do not communicate by sharing memory; instead, share memory by communicating。
最后,我强烈建议您查看 C.A.R.Hoare 的 Communicating Sequential Processes。这个人真是个天才。在这篇 1978 年发表的论文中,他预测了处理器的单核性能最终会遇到瓶颈,芯片制造商们将转而增加处理器内核的数量。他利用这一点的提议对 Go 的设计产生了深远的影响。
脚注
1 - Concurrency is not parallelism by Rob Pike
3 - Goroutine stack size was decreased from 8kB to 2kB in Go 1.4.
4 - Goroutine stacks became contiguous in Go 1.3.
5 - Scheduling of goroutines on golang-nuts by Dmitry Vyukov
6 - Analysis of the Go runtime scheduler by Deshpande et al.
7 - 5 things that make Go fast by Dave Cheney
进一步阅读
如果你有兴趣学习更多关于 Go 的知识,这里有一些关于 Go 语言的精彩讲座
- Go Concurrency Patterns by Rob Pike
- Advanced Go Concurrency Patterns by Sameer Ajmani.
原文链接:blog.nindalf.com/posts/how-g…