协程(coroutine)是一种比线程更轻量级的并发执行单元,它的切换开销和内存栈的占用大小都比线程要小。只要内存足够,在一个线程中可有上万个或更多的协程。除了这些优点,对开发人员来说,在网络编程应用中,采用协程后的业务代码比那些采用异步或事件回调方式的代码更好维护。使用协程的业务逻辑代码表面看上去是同步执行的,编写这些代码时思维是连贯的,更符合人类的思维习惯;而采用异步和回调方式后,业务逻辑代码被分割(分拆)在多个回调方法中,开发人员的思维需要切换,是跳跃的,这样显然容易出错,并且一些场景下还要设计事件或会话上下文数据结构,来保存那些需要在多个回调方法间共享的状态数据。
Golang的一大特色就是从语言层面原生支持协程,在函数或者方法前面加 go关键字就可创建一个协程。
协程的执行线程
协程是在线程中执行的,在Golang中使用环境变量GOMAXPROCS来控制协程使用的线程数,它的缺省值是1,在1.5版本中改为cpu核数。也可在代码中调用
runtime.GOMAXPROCS(n int)函数来设置协程使用的线程数,例如
runtime.GOMAXPROCS(runtime.NumCPU()) 将协程使用的线程数设为cpu个数。
一个Golang进程启动后,该进程中的线程除了协程使用的GOMAXPROCS个线程外,通常还有其他额外的线程。当协程中的代码阻塞在一些系统调用中时,会产生或占用额外的线程,这些线程不算在(不包括)协程占用的GOMAXPROCS个线程中。Golang文档中关于GOMAXPROCS的描述如下:
The GOMAXPROCS variable limits the number of operating system threads that can execute user-level Go code simultaneously. There is no limit to the number of threads that can be blocked in system calls on behalf of Go code; those do not count against the GOMAXPROCS limit. This package's GOMAXPROCS function queries and changes the limit.
当协程阻塞在下列系统调用中时不会产生或占用额外的线程,此时阻塞的协程会被交换出去,协程调度器会调度执行其他可运行的协程。
- network input (socket io)
- sleeping
- channel operations
- blocking on primitives in the sync package.
其他的系统调用会产生额外的线程,例如disk io。如果很多协程同时读写磁盘文件会导致出现很多额外的线程。也就是Golang目前对network socket io会多路复用线程,对disk io不会复用线程。不过github上有个文件io包已经解决了该问题,使用该包读写文件不会出现过多线程。该包见https://github.com/npat-efault/poller,只适用于linux系统。
协程调度器
Golang 的调度器基本上是协作式,而不是抢占式,但也不是完全的协作式调度,在系统调用的函数入口处会有抢占。
例如对如下代码:
package main
import "fmt"
import "runtime"
import "time"
func cpuIntensive(p *int) {
for {
*p++
}
}
func other() {
...
}
func main() {
runtime.GOMAXPROCS(1)
x := 0
go cpuIntensive(&x)
time.Sleep(2* time.Millisecond)
go other()
time.Sleep(100000 * time.Second)
}
上面的代码中,cpuIntensive函数中有个无限循环,当执行go cpuIntensive(&x) 后,它会一直执行不被抢占,导致后面的other协程和main协程饿死,不会执行其中的代码,除非在无限循环中调用runtime.Gosched()让出cpu或加入系统调用函数。
上面的loop循环问题有望在Golang1.6中得到解决,也就是不再需要开发人员显式在代码中加入runtime.Gosched()。
另外当main函数退出时,Golang进程会直接退出,不会等待进程中未完成的协程。
参考文档: