这篇文章解释了我心中很多关于协程的疑问。
以下为正文。
协程(coroutine)的概念已经广为人知,这里就不多说了。作为用户态主动调度的执行单位,协程非常的轻量,并且调度是协作式的,协程可以避免传统多线程程序的上下文切换、调度和锁竞争等开销。
很多人都有这个误解,认为有了协程,就可以用同步程序的方式,写出异步的程序,原先同步的程序和第三方库,也会自动变成异步的。为什么说这是个误解呢,因为要写出有异步效果的程序,只有协程是不够的,还需要有底层IO的支持。协程的调度原本是编程者手工来做的,但是和异步IO结合的话,在发生IO时,自动将IO操作交给异步实现去执行,并让渡出协程的执行权,由调度器去调度执行其他协程。
除去天生支持协程或变种协程的lua, stackless-python, golang 之外,一个本不支持协程的语言也可以通过各种变通方法,来提供对协程的支持。事实上,基本上所有的的语言都有对协程支持的第三方实现,如 Java 的 Kilim, c++ 的boost.coroutine, Python 的 eventlet 和 gevent 等。
问题是,这些语言的底层IO实现,并未对协程调用做处理,其结果是仍然会阻塞这个协程,并没有实现异步的效果。而常用的http /数据库/缓存等lib 都是基于这些语言的底层IO库实现的(如httpclient/libcurl/mysql-connector等),所以使用了这些库的话,在IO 操作时不会让渡执行权,在当前协程阻塞在IO操作上的时候,其他协程也完全无法执行,这比多线程的实现还要糟糕。
除了 IO 之外,第三方lib 如果使用了线程,或是使用了锁,信号量等线程下的同步机制,或是使用了同步的Queue等,在协程环境下使用的时候也会出现各种问题。
所以,java, c++这样的语言,虽然有协程的实现,但是使用范围并不广,因为协程不能利用原先已经存在的大量lib ,需要按照协程的方式重新实现一遍。
一个例外是Python。 Python这样的动态语言,可以使用monkey patch的方式替换系统的IO / thread / queue / lock 等实现, 将其替换成对应的协程。Gevent就是这种做法。
而lua,stackless, golang 这样天生支持协程的语言,所有的IO lib 都是协程实现的,自然可以放心使用。
虽然非常简单,使用协程依然有需要注意的地方。比如:
协程不能有同步IO, 但并非所有的IO操作都一定是异步的。例如文件IO、Pipe、终端输入输出、DNS 解析等,经常没有异步的实现,通常的实现会用线程池进行一个封装,但依然需要在意。
协程只有在发生IO的时候才会让渡执行权,因此存在调度公平性的问题。在大量cpu操作、没有IO的情况下,当前协程会一直占用执行,其他协程得不到执行的机会。所以协程要注意不要有大量密集CPU操作。
协程的开销。协程依然需要占用一定的内存,如goroutine 目前是最小4k 的空间占用。协程切换的代价比较小,但不等于没有,比如stackfull 的协程实现在协程切换是需要做context copy,也有一定的开销。相比较纯异步API 编写的程序,协程的效率通常会差一些。
关于goroutine
goroutine的实现并不完全是传统意义上的协程。在协程阻塞的时候(cpu计算或者文件IO等),多个goroutine会变成多线程的方式执行。golang1.2之后还有类似erlang reduction的机制,来改善goroutine调度的公平性。这个机制只有在函数调用等场合下才会生效,所以效果还比较有限。