一、协程基础
协程和线程类似,协程是一系列的可执行语句,拥有自己的栈、局部变量和指令指针,同时协程又与其他协程共享了全局变量和其他几乎一切资源。协程和线程的主要区别在于,一个多线程程序可以并行运行多个线程,而协程需要彼此协作的运行,即在任意指定的时刻只能有一个协程运行,且只有当正在运行的协程显式的要求被挂起时执行才会暂停。
Lua语言中协程相关的所有函数都被放在表 coroutine中,函数create用于创建新协程,该函数只有一个参数,即协程要执行的代码的函数。函数create返回一个” thread "类型的值,即新协程。
函数create 的参数可以是一个匿名函数:
> co=coroutine.create(function() print("hello,coroutine") end)
> print(co)
thread: 0x2609628
> print(type(co))
thread
一个协程有以下四种状态,即挂起( spended )、运行( running) 、正常( normal )和死亡( dead ),可以通过函数 coroutine.status 来检查协程的状态:
> coroutine.status(co)
suspended
当一个协程被创建时,它处于挂起状态,即协程不会在被创建时自动运行。函数 coroutine.resume 用于启动或再次启动一个协程的执行,并将其状态由挂起改为运行。
> coroutine.resume(co)
hello,coroutine
true
> coroutine.status(co)
dead
协程运行完之后就会进入死亡状态,如果要在交互模式下运行上述代码 ,最好在最后一行加上一个分号来阻止输出函数 resume的返回值。
协程看上去也就是一种复杂的调用函数的方式,协程的真正强大之处在于函数 yield ,该函数可以让一个运行中的协程挂起自己,然后在后续恢复运行。
co=coroutine.create(function()
for i=1,10 do
print("co",i)
coroutine.yield()
end
end)
for i=0,10 do
coroutine.resume(co)
print(co,coroutine.status(co))
end
print(coroutine.resume(co))
执行结果:
co 1
thread: 0x1d493d8 suspended
co 2
thread: 0x1d493d8 suspended
co 3
thread: 0x1d493d8 suspended
co 4
thread: 0x1d493d8 suspended
co 5
thread: 0x1d493d8 suspended
co 6
thread: 0x1d493d8 suspended
co 7
thread: 0x1d493d8 suspended
co 8
thread: 0x1d493d8 suspended
co 9
thread: 0x1d493d8 suspended
co 10
thread: 0x1d493d8 suspended
thread: 0x1d493d8 dead
false cannot resume dead coroutine
可以看到,在挂起期间发生的活动都发生在协程调用 yield 期间,当唤醒协程时,函数 yield 才会最终返回,然后协程会继续执行直到遇到下一个 yield 或执行结束。在最后一次调用 resume 时,协程体执行完毕并返回,不输出任何数据,如果试图再次唤醒它,函数resume 将返回 false和一条错误信息。
注意:函数 resume 也运行在保护模式中 因此,如果协程在执行中出错, Lua 语言不会显示错误信息,而是将错误信息返回给函数resume。
当协程A唤醒协程B时,协A既不是挂起状态(因为不能唤醒协程A),也不是运行状态(因为正在运行的协程是B)。所以,协程A此时的状态就被称为正常状态。
Lua 语言中一个非常有用的机制是通过一对 resume-yield 来交换数据;第一个 resume数(没有对应等待它的 yield )会把所有的额外参数传递给协程的主函数。
co = coroutine.create(function (a, b, c)
print("co",a,b+1,c+2)
end)
co outine.resume(co, 1, 2, 3)
co 1 3 5
在函数 coroutine. esume 的返回值中,第一个返回值为true 表示没有错误,之后的返回值对应函数 yield 的参数。当一个协程运行结束时,主函数所返回的值都将变成对应函数 resume 的返回值。
co=coroutine.create(function(x)
print("co1",x)
print("co2",coroutine.yield())
end)
coroutine.resume(co,"hello")
coroutine.resume(co,4,5);
co1 hello
co2 4 5
二、生产者-消费者问题
协程最经典示例之一就是生产者-消费者问题。在生产者-消费者问题中涉及两个函数,一个函数不断地产生值,另一个函数不断的消费这些值。
示例,使用过滤器的生产者-消费者模型,过滤器位于生产者和消费者之间,用于完成一些对数据进行某种变换的任务。过滤器 filter 既是一个消费者又是一个生产者,它通过唤醒一个生产者来获得新值,然后又将变换后的值传递给消费者。在前面代码中添加一个过滤器以实现在每行的起始处插入行号。
function receive(prod)
local status,value=coroutine.resume(prod)
return value
end
function send(x)
coroutine.yield(x)
end
function producer()
return coroutine.create(function()
while true do
local x=io.read()--new info
send(x)
end
end)
end
function filter(prod)
return coroutine.create(function()
for line=1,math.huge do
local x=receive(prod)
x=string.format("%5d %s",line,x)
send(x)
end
end)
end
function consumer(prod)
while true do
local x=receive(prod)
io.write(x,"\n")
end
end
consumer(filter(producer()))
代码的最后一行只是简单地创建出所需的各个组件,将这些组件连接在一起,然后启动消费者。
执行结果:
hello
1 hello
world
2 world
coroutine
3 coroutine
4
let's go
5 let's go
三、将协程用作迭代器
用协程来实现迭代器很合适,协程为实现这类任务提供了一种强大的工具;同时,协程最关键的特性是能够颠倒调用者与被调用者之间的关系。有了这种特性,在编写迭代器时就无须担心如何保存连续调用之间的状态了。
function printResult(a)
for i=1,#a do
io.write(a[i]," ")
end
io.write("\n")
end
function permgen(a,n)
n=n or #a
if n<=1 then
--printResult(a)
coroutine.yield(a)
else
for i=1,n do
-- 第i个元素当作最后一个
a[n],a[i]=a[i],a[n]
permgen(a,n-1)
-- 恢复
a[n],a[i]=a[i],a[n]
end
end
end
function permutations(a)
return coroutine.wrap(function() permgen(a) end)
-- local co=coroutine.create(function() permgen(a) end)
-- return function()--迭代函数
-- local code,res=coroutine.resume(co)
-- return res
-- end
end
--permgen({1,2,3,4,5})
for p in permutations{"a","b","c"} do
printResult(p)
end
执行结果:
b c a
c b a
c a b
a c b
b a c
a b c
函数 permutations使用了 一种常见的模式,就是将唤醒对应协程的调用封装在一个函数中。wrap函数与函数create 类似,函数 wrap 也用来创建一个新的协程,但不同的是,函数 wrap 返回的不是协程本身而是一个函数,当这个函数被调用时会唤醒协程。与原始的函数resume 不同,该函数的第一个返回值不是错误代码,当遇到错误时该函数会抛出异常。函数 coroutine. wrap 比函数 coroutine.create 更易于使用。 不过,该函数缺乏灵活性,无法检查通过函数 wrap 所创建的协程的状态 ,也无法检查运行时的异常。
四、事件驱动式编程
协程可以让我们使用事件循环来简化循环的代码,其核心思想是使用协程运行主要代码,即在每次调用库时将回调函数设置为唤醒协程的函数然后让出执行权。
实现一个在异步 I/O 库上运行传统同步代码的示例:
async-lib.lua
local cmdQueue={}
local lib={}
function lib.readline(stream,callback)
local nextCmd=function()
callback(stream:read())
end
table.insert(cmdQueue,nextCmd)
end
function lib.writeline(stream,line,callback)
local nextCmd=function()
callback(stream:write(line))
end
table.insert(cmdQueue,nextCmd)
end
function lib.stop()
table.insert(cmdQueue,"stop")
end
function lib.runloop()
while true do
local nextCmd=table.remove(cmdQueue,1)
if nextCmd=="stop" then
break
else
nextCmd()
end
end
end
return lib
user.lua
local lib =require "async-lib"
function run(code)
local co=coroutine.wrap(function()
code()
lib.stop() --结束时停止事件循环
end)
local lib =require "async-lib"
function run(code)
local co=coroutine.wrap(function()
code()
lib.stop() --结束时停止事件循环
end)
co() --启动协程
lib.runloop() --启动事件循环
end
function putline (stream, line)
local co = coroutine.running() -- 调用协程
local callback = (function () coroutine.resume(co) end)
lib.writeline(stream, line, callback)
coroutine.yield()
end
function getline (stream, line)
local co = coroutine.running() -- 调用协程
local callback = (function (l) coroutine.resume(co, l) end)
lib.readline(stream, callback)
local line = coroutine.yield()
return line
end
run(function()
local t={}
local inp=io.input()
local out=io.output()
while true do
local line=getline(inp)
if not line then break end
t[#t+1]=line
end
for i=#t,1,-1 do
putline(out,t[i] .. "\n")
end
end)
五、协程相关API说明
API | 参数 | 返回值 | 说明 |
---|---|---|---|
create(f) | 函数,作为协程运行的主函数 | 返回创建好的协程 | 该函数只负责创建协程,而如果要运行协程,还需要执行resume操作 |
resume(co,[value1,…]) | 传入的第一个参数是create()返回的协程,剩下的参数是传递给协程运行的参数 | resume成功的情况下返回true 以及上一次yield函数传入的参数;失败的情况下返回false以及错误信息 | 第一 次执行resume操作时,会从create传人的函数开始执行,之后会在该协程主函数调用yield 的下一个操作开始执行,直到这个函数执行完毕。调用resume操作必须在主线程中 |
running | 无 | 返回当前正在运行的协程,如果在主线程调用将返回nil | |
status | 无 | 返回当前协程的状态,有dead、runing、suspend、normal | |
warp | 与create类似,传入协程运行的主函数 | 返回创建好的协程 | wrap 函数相当于结合了create和resume。所不同的是,wrap函数返回的是创建好的协程,下次直接传入参数调用该协程即可,无需调用resume。 |
yield | 变长参数,这些是返回给此次resume函数的返回值 | 返回下一个resume操作传入的参数值 | 挂起当前协程的运行,调用yield操作必须在协程中 |
六、总结
- 协程是skynet 架构最小的运行的单元。
- 协程是一段独立的执行线程。
- 一个 lua 虚拟机中可以有多个协程,但同时只能有一个协程在运行。