Lua 中的协程
最早接触Coroutine是在Programming in Lua中,如今这本书已经出到第三版了,第9章介绍了lua中Coroutine的API,读过一遍后,没不太明白,后来又读了一篇关于这方面的论文,作者也是原书的作者,论文将Coroutine机制阐述的很详细,很值得一读。
什么是Coroutine
我们首先来看一下lua的作者对Coroutine是这样解释的:
“Coroutine和线程类似,是一段可执行代码,它拥有自己的stack,自己的局部变量和自己的stack pointer,可以和其它Coroutine共享全局变量。和线程的重要区别是:线程可以并发执行,Coroutine只能协作执行,即CPU的某个时刻只会有一个Coroutine被执行,这个Coroutine终止的条件是自己执行完毕或者它自己明确的要求被停止(Coroutine是不允许从外部被终止的)。”
Coroutine翻译过来叫做协程,顾名思义是一种协同工作的概念。举个例子,就好比A,B两个人要共同装修一栋房子,A干了一会儿对B说:“我木头运进来了,该你了铺地板了,等你干完了我再来刷油漆”(注意是A自己说的),B收到后,过来将它的工作干完,于是A,B就一种协同工作的方式。注意协同并不等于同步,比如当A说完:”我干完了,该你了”之后,A完全可以去干别的事情,比如去买一瓶啤酒再吃顿饭,等B铺完地板后,再接着干活,而不是坐在那里傻等着B完成它的任务。因此从A的角度看,和B一起干活这件事是异步的,A的时间并没有因为B而block。但是需要注意的是,异步并不等于多线程,多线程强调的是并行计算,任务之间同时进行而且没有联系;而协程是协同工作,任务之间彼此依赖并且有先后关系,比如上面例子中A和B是不能同时干活的,而对A来说也不能同时运木头和买啤酒。
实际上协程这个概念早在60年代就成型了,只是现代的主流编程语言都没有加入对它的实现。在上述论文中有提到:
“设计编程语言的人缺乏对Coroutine的有效理解,而提出Coroutine的作者(Marlin)也只是在Simula中实现了Coroutine而且实现的超复杂”
后来,有两伙人开始重拾Coroutine,一伙人提出了用Coroutine来实现非抢占式(non-preemptive)多线程,还取了一个名字叫:collaborative multithreading——协作式多线程。和传统的抢占式多线程(preemptive)相比,这种方式开销很小(因为不涉及到内核,所以不存在context switching带来的开销),又能规避线程带来的很多问题,在某些场景很有用。其中最具代表性的是Windows中的Fiber机制,在MSDN中有描述。另一伙人将Coroutine用到了脚本语言中,包括Python,Perl和Lua等,Lua在当时是为数不多支持协程的语言,但是没有多少人知道,直到现在去Google搜索什么是协程,大多数的答案仍是以Python的Generator为分析的例子,而对于JavaScript,更是在ES6出来后,才提供了对协程的API(generator)的支持。这一方面说明Lua的小众,另一方面说明在Lua的应用场景中需要用到并发,而使用协程可以摆脱语言对操作系统(内核)的依赖,轻量级的异步任务。
Lua中的Coroutine
接着我们来看看如何用在Lua中使用Coroutine。首先我们可以通过create
方法创建一个协程对象
co = coroutine.create(function() print("hi") end)
print(co,type(co)) --thread: 0x1001082a0
在lua中,一个协程对象有四种状态:suspend
,dead
,running
和normal
,当一个新的协程被创建时,它是suspend状态:
print(coroutine.status(co)) --suspend
然后我们可以使用resume
方法来执行协程任务,其状态会变为running,当任务完成后,状态会变为dead
:
coroutine.resume(co) --hi
--coroutine执行完成后,状态变为dead
print(coroutine.status(co)) --dead
--dead之后不能resume了
print(coroutine.resume(co)) --false cannot resume dead coroutine
上面代码通常没有什么意义,因为这种使用协程的方式和使用普通函数没有区别,并不能实现任务的异步执行,使用协程的正确方式是和yield
搭配使用
co = coroutine.create(function()
for i=1,3 do
print("index: ",i)
coroutine.yield()
end
end)
coroutine.resume(co)
上面代码中,当执行resume
方法后,协程函数会执行,首先输出index: 1
,接着执行了yield
。yield()
的作用是挂起协程,即中断该协程函数的执行。由前面的介绍可知,每个协程函数都有自己的stack,执行yield()
后,协程函数的栈帧会被保存,栈里的信息也会被保存,而程序的控制权则转移给了主函数。
我们也可以向协程函数中出传参,这里分为两种情况,一种是协程函数中没有yield
,那么函数执行完后协程对象状态变为dead:
--第一种情况
--使用wrap
co = coroutine.wrap(function(a) return 2*a end)
b = co(20)
print(b) -- 40
--使用create
co = coroutine.create(function(a) return 2*a end)
b,v1 = coroutine.resume(co,20)
print(b,v1) --true 40
第二种情况是当协程函数中有yield
方法时,当执行到yield(arg)
,协程函数的返回值将是函数栈顶变量的值,即arg
的值。
--使用wrap
co = coroutine.wrap(function(a)
local c = coroutine.yield(a+1)
print("main func a: ",a)
return 2*a
end)
d = co(20) --hit yield
print(d) -- d = 21
--从yield后面执行
d = co(20)
print(d) -- 40
--使用create
co = coroutine.create(function(a)
local c = coroutine.yield(a+1)
print("main func c: ",c)
return 2*a
end)
b,v = coroutine.resume(co,20)
print(b,v) -- true,21
b,v = coroutine.resume(co,20)
print(b,v) -- true,40
接下来我们来使用协程模拟一个简单的生产者,消费者的模式
producer = coroutine.create(function()
while true do
local x = io.read()
print("producer create: ",x)
coroutine.yield(x)
end
end)
function consumer ()
while true do
status,x = coroutine.resume(producer)
if x == "ok" then
print("consumer quit: ",x)
break
else
print("consumer get: ",x)
end
end
end
consumer()
上述代码中producer
是一个协程,它的协程函数是一个while
循环,不断的从console中读入字符。consumer
是一个普通函数,它也是一个while
循环,通过不断调用协程的对象的resume
方法来获取数据。一次典型的生产消费过程如下:
consumer
调用producer
的resume
方法索要数据,协程被唤醒producer
从IO中读取数据,”生产”一个x
,然后执行yield
,将x
push到某个地方后,协程被挂起- 此时控制权转回
consumer
,resume
执行完成后得到了x
后并输出。 - 进入下一次循环,重复1,直到生产出的
x
为ok
,循环退出
➜ Desktop lua async.lua
1
producer create: 1
consumer get: 1
2
producer create: 2
consumer get: 2
3
producer create: 3
consumer get: 3
4
producer create: 4
consumer get: 4
ok
producer create: ok
consumer quit: ok