协程(coroutine)的概念已经广为人知,这里就不多说了。作为用户态主动调度的执行单位,协程可以避免传统多线程程序的上下文切换、调度和锁竞争等开销。
前一段时间一个小师弟过来面试,提到在阿里实习时,为了方便的编写高并发长连接程序,调研发现了了一个java的协程实现。我于是便问他结果如何,用这个真的能够达到你的目的吗?他就答不出来了,只是在机械的重复「协程相比回调更适合异步编程」之类的。
很多人都有这个误解,认为有了协程,就可以用同步程序的方式,写出异步的程序,原先同步的程序和第三方库,也会自动变成异步的。为什么说这是个误解呢,因为要写出有异步效果的程序,只有协程是不够的,还需要有底层IO的支持。在发生IO时,要将IO操作交给异步实现去执行,并让渡出协程的执行权,由调度去调度执行其他协程。
事实上,协程只是用户态实现的一种调度方法而已,除去天生支持协程(或协程的类似物)的lua, stackless-python, golang等之外,一个本不支持协程语言要实现协程是很容易的事情。事实上,基本上所有的的语言都有对协程支持的第三方实现。
问题是,这些语言的底层IO实现,并未对协程调用做处理,其结果是仍然会阻塞这个协程,并没有实现异步的效果。而常用的http /数据库/缓存等lib 都是基于这些语言的底层IO库实现的(如httpclient/libcurl/mysql-connector等),所以使用了这些库的话,在IO 操作时不会让渡执行权,在当前协程阻塞在IO操作上的时候,其他协程也完全无法执行,这比多线程的实现还要糟糕。
所以,java,c#,c++这样的语言,虽然有协程的实现,但是使用起来却基本上没有意义。没有底层IO实现的支持,协程也就只能用来谢谢生产者-消费者这样的demo而已,除非协程本身提供新的支持协程的IO 底层库并实现所需要的http / database / cache 等的lib。
lua,golang等在其系统的底层IO实现已经做了处理,所以才可以使用协程。即使如此,依然有需要注意的地方。比如:
- 并非所有的IO操作都一定是异步的。例如golang只对socket IO有异步实现,文件IO、Pipe、终端输入输出等依然是阻塞的,所以在goroutine要特别注意这些操作。
- 协程只有在发生IO的时候才会让渡执行权,因此存在调度公平性的问题。在大量cpu操作、没有IO的情况下,当前协程会一直占用执行,其他协程得不到执行的机会。所以协程要注意不要有大量密集CPU操作。
关于Python
Python的IO实现虽然也没有对协程做处理,但是Python这样的动态语言,可以使用monkey patch的方式替换系统的IO实现。gevent就是这种做法,这也是使用gevent的greenlet的时候,一般必须要在使用前做monkey patch的原因
关于goroutine
goroutine的实现并不完全是传统意义上的协程。在协程阻塞的时候(cpu计算或者文件IO等),多个goroutine会变成多线程的方式执行。golang1.2之后还有类似erlang reduction的机制,来改善goroutine调度的公平性。这个机制只有在函数调用等场合下才会生效,所以效果还比较有限。
关于read/write hook
腾讯广州研究院的微信团队在一篇文章中透露出,他们使用hook read/write系统调用和swapcontext让出执行权的方式来实现类似于协程的效果,但是这个实现应该只在linux上可用,并且swapcontext还是要比协程切换更重一些。