Goroutine是如何工做的?

翻译原文连接 转帖/转载请注明出处
英文原文连接 发表于2014/02/24
html

Go语言

若是你刚刚接触Go语言,或者说你并不理解“并发不等于并行”这句话的含义,那么Rob Pike的讲座值得一看(在youtube上)。这个视频有30分钟长,我保证花30分钟看这段视频是很是值得的。程序员

这里摘录一段他提到的并发和并行之间的区别:“当你们听到并发这个词的时候,他们每每想到的是并行。并行是一个相关,但却彻底不一样的概念。当咱们编程的时候,并发指的是多个独立运行的进程,而并行是指同时运行的多个计算。并发是为了一会儿处理不少东西。并行是为了同时作不少事情。” [1] (注:这里的概念有点绕。其实本质的区别在“同时”这个词上。并行强调的时候几个进程同时进行。而并发指的是运行多个进程,但这些进程并不须要同时被执行。它们能够是被调度在同一个CPU分时运行的。)golang

Go为咱们写并发程序提供了便利。它提供了goroutine以及它们之间通讯的功能。在这里咱们主要讨论goroutine。编程

Goroutine和线程的区别

Go语言使用的是goroutine,而像Java这样的语言大多使用线程。它们之间的区别是什么呢?让咱们从三个方面来看看它们的区别:内存占用,建立和销毁,以及切换开销。网络

内存占用

建立一个goroutine不须要太多的内存 - 大概2KB左右的栈空间。若是须要更多的栈空间,就从堆里分配额外的空间来使用。2 新建立的线程会占用1MB的内存空间(这大约是goroutine的500倍)。这还不包括守护页(guard page)的空间。守护页是用来保护线程之间的内存空间不会被相互窜改。[7]并发

所以一个处理不少请求的服务能够为每一个请求建立一个goroutine。可是若是为每一个请求去建立一个线程,那么它很快就会碰到OutOfMemoryError。这不是Java独有的问题,任何使用操做系统线程做为主要并发手段的编程语言都会碰到这个问题。编程语言

建立和销毁的开销

线程须要从操做系统里请求资源并在用完以后释放回去,所以建立和销毁线程的开销很是大。为了不这些开销,咱们一般的作法是维护一个线程池。Goroutine的建立和销毁是由运行环境(runtime)完成的。这些操做的开销就比较小。Go语言不支持手工管理goroutine。函数

切换开销

当一个线程阻塞的时候,另一个线程须要被调度到当前处理器上运行。线程的调度是抢占式的(preemptively)。当切换一个线程的时候,调度器须要保存/恢复全部的寄存器。这包括16个通用寄存器,程序指针(program counter),栈指针(stack pointer),段寄存器(segment registers)和16个XMM寄存器,浮点协处理器状态,16个AVX寄存器,全部的特殊模块寄存器(MSR)等。当在线程间快速切换的时候这些开销就变得很是大了。工具

Goroutine的调度是协同合做式的(cooperatively)。当切换goroutine的时候,调度器只须要保存和恢复三个寄存器 - 程序指针,栈指针和DX。切换的开销就小多了。oop

前面已经谈到了,goroutine的数目会比线程多不少,但这并不影响切换的时间。有两个缘由:第一,只有能够运行的goroutine才会被考虑,正在阻塞的goroutine会被忽略。第二,现代的调度器的复杂度都是O(1)的。这意味着选择的数目(线程或者是goroutine)不会影响切换的时间。[5]

Goroutine的运行

前面谈到,运行环境负责goroutine的建立,调度和销毁。运行环境被会分配一些线程,用来运行全部的goroutine。在任何一个时间点,每一个线程只会运行一个goroutine。若是一个goroutine被阻塞,另一个goroutine会来替换它在对应的线程上运行。[6]

由于goroutine的调度是协同合做式的,若是一个goroutine不停的循环,其它的goroutine就没有机会被调度运行了。在Go 1.2里,这个问题的解决办法是在调用一个函数的时候去偶尔触发Go的调度器。这样一个循环里若是调用了没有被内联的函数,它就能够被抢占了。

Goroutine的阻塞

Goroutine是廉价的,在下面这些阻塞状况下它们也不会形成运行的线程被阻塞:

  • 网络收发

  • 睡眠

  • channel操做

  • sync包里的一些会阻塞的基本操做

即便建立了成千上万的goroutine而且大多数被阻塞了,也不会形成太多的系统资源浪费。由于运行环境会调度另外的goroutine来运行。

简而言之,goroutine是对线程的轻量化抽象。Go语言的程序员不须要直接操做线程。与此同时操做系统也不知道goroutine的存在。从操做系统的角度来看,一个Go程序有点像一个事件驱动的C程序。[5]

线程和处理器

虽然咱们不能直接控制运行环境建立多少线程,咱们能够设置程序使用的处理器核数。这是经过调用runtime.GOMAXPROCS(n)函数设置GOMAXPROCS变量来实现的。(注:也能够经过直接设置环境变量来控制)。增长处理器核数并不意味着程序性能的提升。这取决于程序自己的设计。你的程序须要用到多少个内核数能够用剖析(profiling)工具来找到答案。

结束语

和其它语言相似,避免多个goroutine同时访问一个共享资源是很是重要的。goroutine之间,最好是用channel来传输数据。有兴趣的能够读一读“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

    2. Effective Go: Goroutines

    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 by Dmitry Vyukov

    6. Analysis of the Go runtime scheduler

    7. 5 things that make Go fast by Dave Cheney

相关文章
相关标签/搜索