协程(goroutine)
Go不支持创建系统线程,所以协程是一个Go程序内部唯一的并发实现方式。每个Go程序启动的时候只有一个对用户可见的协程,我们称之为主协程。一个协程可以开启更多其它新的协程。
在Go中,开启一个新的协程是非常简单的,我们只需要在一个函数调用之前使用一个go
关键字,即可让此函数调用运行在一个新的协程之中。当此函数调用退出后,这个新的协程也随之结束。(不管其本身是否执行结束)我们可以称此函数调用为一个协程调用(或者为此协程的启动调用)。一个协程调用的所有返回值(如果存在的话)必须被全部舍弃。
并发同步(concurrency synchronization)
不同的并发计算可能会共享一些资源,其中共享内存资源最为常见。在一个并发程序中,常常会发生下面的情形:
- 在一个协程向一段内存写数据的时候,另一个协程从此内存段读数据,结果导致读出的数据完整性得不到保证
- 在一个协程向一段内存写数据的时候,另一个计算也向此段内存写数据,结果导致被写入的数据的完整性得不到保证
这些情形被称为数据竞争(data race)。并发编程的一大任务就是要调度不同协程,控制它们对资源的访问时段,以使数据竞争的情况不会发生。此任务常称为并发同步(或者数据同步)。Go支持几种并发同步技术。并发编程中的其它任务包括:
- 决定需要开启多少协程
- 决定何时开启、阻塞、解除阻塞和结束哪些协程
- 决定如何在不同的协程中分担工作负载
协程的状态
一个活动中的协程可以处于两个状态:运行状态和阻塞状态。一个协程可以在这两个状态之间切换。注意,一个处于睡眠中的(通过调用time.Sleep
)或者在等待系统调用返回的协程被认为是处于运行状态,而不是阻塞状态。
当一个新协程被创建的时候,它将自动进入运行状态,一个协程只能从运行状态而不能从阻塞状态退出。一个处于阻塞状态的协程不会自发结束阻塞状态,它必须被另外一个协程通过某种并发同步方法来被动的结束阻塞状态。
如果一个运行中的程序当前所有的协程都处于阻塞状态,则这些协程将永远阻塞下去,程序将被视为死锁了。当一个程序死锁后,官方标准编译器的处理是让这个程序崩溃。
协程的调度
并非所有处于运行状态的协程都在执行。在任一时刻,只能最多有和逻辑CPU数目一样多的协程在同时执行。我们可以调用runtime.NumCPU
函数来查询当前程序可利用的逻辑CPU数目
标准编译器采纳了一种被称为M-P-G模型
的算法来实现协程调度。其中,M表示系统线程,P表示逻辑处理器(并非上述的逻辑CPU),G表示协程。大多数的调度工作是通过逻辑处理器(P)来完成的。逻辑处理器像一个监工一样通过将不同的处于运行状态的协程(G)交给不同的系统线程(M)来执行。一个协程在同一时刻只能在一个系统线程中