什么是coroutine?
coroutine是一种古老的计算模式,中文翻译叫做“协程”,是一种多线程编程思路,早在1969年被提出。coroutine本质上是一种轻量级的thread,它的开销会比使用thread少很多。多个coroutine可以按照次序在一个thread里面执行,一个coroutine如果处于block状态,可以交出执行权,让其他的coroutine继续执行。coroutine和thread最大的区别在于,一个coroutine是显式的将执行权交给另一个coroutine,而thread是通过操作系统的调度器来共享CPU资源。下图是两个coroutine之间的协作过程。
coroutine 怎么用?
以Lua语言为例,介绍coroutine的创建,运行,和终止。
- 创建:
创建一个 coroutine 需要调用一次 coroutine.create 。它只接收单个参数,这个参数是 coroutine 的主函数。 create 函数仅仅创建一个新的 coroutine 然后返回它的控制器(一个类型为 thread 的对象);它并不会启动 coroutine 的运行。
- 运行:
当Coroutine被第一次调用到的时候,它将从起始处开始执行,一旦遇到具有yield语义的语句的时候,就是返回给另外一个Coroutine或者调用者,而在接下来被调用的时候,就从yield语句下面的一条语句继续执行直到遇到新的yield语句或者Coroutine的结束。
- 终止:
coroutine 可以通过两种方式来终止运行: 一种是正常退出,指它的主函数返回(最后一条指令被运行后,无论有没有显式的返回指令); 另一种是非正常退出,它发生在未保护的错误发生的时候。第一种情况中, coroutine.resume 返回 true ,接下来会跟着 coroutine 主函数的一系列返回值。第二种发生错误的情况下, coroutine.resume 返回 false ,紧接着是一条错误信息。
说得很抽象,下面看一段Lua代码:
function foo (a)
print("foo", a)
return coroutine.yield(2*a)
end
co = coroutine.create(function (a,b)
print("co-body", a, b)
local r = foo(a+1)
print("co-body", r)
local r, s = coroutine.yield(a+b, a-b)
print("co-body", r, s)
return b, "end"
end)
print("main", coroutine.resume(co, 1, 10))
print("main", coroutine.resume(co, "r"))
print("main", coroutine.resume(co, "x", "y"))
print("main", coroutine.resume(co, "x", "y"))
代码中定义了一个函数foo,然后调用coroutine.create创建了一个coroutine,它的运行路线到end处结束。接下来的四个print是主线程的代码,线程从这里开始执行。第一次调用coroutine.resume,coroutine 从主函数的第一行(即print("co-body", a, b))开始运行。在调用foo函数的时候,遇到一个coroutine.yield语句,控制权交还给主线程,主线程执行第二个print中的coroutine.resume再次将控制权交给coroutine,这次coroutine从foo函数之后的一条语句开始运行,以此类推。
于是当你运行以上程序,将得到如下输出结果:
co-body 1 10
foo 2
main true 4
co-body r
main true 11 -9
co-body x y
main true 10
end
main false cannot resume dead coroutine
coroutine性能如何?
coroutine的编程思路类似Java中的wait/notify。于是有以下对比程序:
lua code
co = coroutine.create(function(loops)
for i = 1, loops do
coroutine.yield()
end
end)
local x = os.clock()
local loops = 100 * 1000 * 1000
coroutine.resume(co, loops)
for i = 1, loops do
coroutine.resume(co)
end
print(string.format("elapsed time: %.2f\n", os.clock() - x))
java code
public class TestWait {
public static void main(String[] args) {
WaitClass wc = new WaitClass();
wc.start();
int loops = 100 * 1000 * 1000;
long t1 = System.currentTimeMillis();
for (int i = 0; i < loops; i++) {
synchronized (wc) {
wc.notify();
}
}
long t2 = System.currentTimeMillis();
System.out.println("elapsed time: " + (t2 - t1) / 1000l);
}
}
class WaitClass extends Thread {
public void run() {
while (true) {
synchronized (this) {
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
运行结果如下:
Lua elapsed time: 53.36
Java elapsed time: 51
CPU占用
运行环境:4core XEON
Lua 1CPU 100%, 其他CPU0%,total 25% (其中CPU sys 0%)
Java 2个CPU 40%-50%, 其他CPU 0%,total 25% (其中CPU sys 5%-10%)
从结果看,coroutine只利用了一个CPU, 因为它实际上是一个串行运算,这也是coroutine不会有传统并行计算的访问冲突的原因。
Java利用了2个CPU, 各占用了50%的CPU时间运行和50%的时间等待,和设计也一致。另外Java用了5-10%的sys CPU时间用于线程context switch。
其实,Java为了调用wait/notify,使用了同步锁,但性能上不比coroutine差。既然这样,基于负载均衡的考虑,coroutine没有任何优势。那么,为什么要用coroutine呢?
为什么要用coroutine?
因为coroutine有它的优点:
•每个coroutine有自己私有的stack及局部变量。
•同一时间只有一个coroutine在执行,无需对全局变量加锁。
•顺序可控,完全由程序控制执行的顺序。而通常的多线程一旦启动,它的运行时序是没法预测的,因此通常会给测试所有的情况带来困难。
其典型的应用场景如下:
•状态机。
•异步IO操作:异步IO操作通常是发起一个IO请求,由操作系统完成以后回调指定的方法或者使用其它方式通知。
•高并发网络服务器,高并发服务器除了要处理场景一的情况外,可能还要结合场景二,多线程方案有时候完全不能接受,更多的是基于事件、异步IO或者是混合事件和多线程的模型。
•客户端并发应用
Bibliography
1.lua手册:http://manual.luaer.cn/2.11.html
2.后端技术博客:http://timyang.blog.51cto.com/1539170/307673