Lua 协同程序(coroutine)与线程比较类似:拥有独立的堆栈,独立的局部变量,独立的指令指针,同时又与其它协同程序共享全局变量和其它大部分东西。
线程和协同程序区别
线程与协同程序的主要区别在于,一个具有多个线程的程序可以同时运行几个线程,而协同程序却需要彼此协作的运行。在任一指定时刻只有一个协同程序在运行,并且这个正在
运行的协同程序只有在明确的被要求挂起的时候才会被挂起。协同程序有点类似同步的多线程,在等待同一个线程锁的几个线程有点类似协同。
协同程序的基本函数
方法 | 描述 |
---|---|
coroutine.create() | 创建coroutine,返回coroutine, 参数是一个函数,当和resume配合使用的时候就唤醒函数调用 |
coroutine.resume() | 重启coroutine,和create配合使用 |
coroutine.yield() | 挂起coroutine,将coroutine设置为挂起状态,这个和resume配合使用能有很多有用的效果 |
coroutine.status() | 查看coroutine的状态 注:coroutine的状态有三种:dead,suspend,running,具体什么时候有这样的状态请参考下面的程序 |
coroutine.wrap() | 创建coroutine,返回一个函数,一旦你调用这个函数,就进入coroutine,和create功能重复 |
coroutine.running() | 返回正在跑的coroutine,一个coroutine就是一个线程,当使用running的时候,就是返回一个corouting的线程号 |
co = coroutine.create(function ()
print("hi")
end)
print(co) --> thread: 0x8ca6c6c
print(coroutine.status(co)) -->suspended
coroutine.resume(co) --> hi
print(coroutine.status(co)) -->dead
通过上面的这个例子可以看出,在使用create()函数成功创建出协同程序后,该协同程序不会立即执行。此时获取它的状态,依旧是挂起状态。这一点是与线程创建不同的,线程创建成功后,会立即执行其内置函数。
如果想要启动协同程序,可以使用resume()函数,使协同程序的内置函数开始执行,从上述例子中可以看到,执行后,打印"hi"。在内置函数执行完成后,协同程序就结束了。此时在获取协同程序的状态,就是dead状态了。
上面的这个例子,相当于一个比较繁琐的函数调用。而协同程序的强大之处,在于yield()函数,它可以将正在执行的协同程序挂起。
co =coroutine.create(function ()
for i=1,10 do
print("co", i)
coroutine.yield()
end
end)
coroutine.resume(co) --> co 1
print(coroutine.status(co)) --> suspended
coroutine.resume(co)-->co 2
print(coroutine.status(co)) --> suspended
coroutine.resume(co)-->co 3
print(coroutine.status(co)) --> suspended
coroutine.resume(co)-->co 4
print(coroutine.status(co)) --> suspended
coroutine.resume(co)-->co 5
coroutine.resume(co)-->co 6
coroutine.resume(co)-->co 7
coroutine.resume(co)-->co 8
coroutine.resume(co)-->co 9
coroutine.resume(co)-->co 10
print(coroutine.status(co))
coroutine.resume(co) --prints nothing
print(coroutine.status(co))
print(coroutine.resume(co))
该段程序执行结果如下:
co 1
suspended
co 2
suspended
co 3
suspended
co 4
suspended
co 5
co 6
co 7
co 8
co 9
co 10
suspended
dead
false cannot resume dead coroutine
从结果可以看出,该协同程序的内置函数在执行时,遇到yield()函数就会被挂起。只有使用resume()函数时,才会继续执行。最后一次resume()后,该协同程序被销毁。此时,如果在对该协同程序使用resume()函数,会得到错误信息,警告无法恢复已经销毁的协同程序。
注意:resume 运行在保护模式下,因此如果协同内部存在错误 Lua并不会抛出错误而 是将错误返回给 resume 函数。
resume()函数
resume()函数除了第一个参数是协同程序外,还可以继续传递其他参数,如:
local co = coroutine.create(function(name)
print(name);
end);
coroutine.resume(co, "resume param");
执行结果为:
resume param
如果协同程序正确执行,错误信息这个返回值自然就是nil了。
然后,这里还有一个规则,那就是yield函数的参数可以传递到resume的第二个返回值里。如:
local co = coroutine.create(function(name)
print(name);
coroutine.yield("yield param");
end);
local result, msg = coroutine.resume(co, "resume param");
print("msg:" .. msg);
print(coroutine.status(co))
result, msg = coroutine.resume(co);
print(result, msg)
print(coroutine.status(co))
result, msg = coroutine.resume(co, "resume param");
print(result, msg);
执行结果为:
resume param --协同程序被启动,由resume()函数传递参数
msg:yield param --协同程序被挂起,执行结果和挂起信息都通过resume()函数返回值传递
suspended --协同程序被挂起
true nil --协同程序继续执行,执行完毕即销毁,未再被挂起,内置函数无返回值,msg信息为nil
dead --协同程序被销毁
false cannot resume dead coroutine--resume()函数调用失败,msg为错误信息
resume()的第二个返回值,是yield()函数的第二个参数。这样的前提是,resume()函数的返回值是true,否则的话,第二个返回值是错误信息。如果未被挂起,第二个返回值会被设置为协同程序内置函数的返回值。如果yield()函数参数为空,则resume()函数第二个返回值为nil。
注意:从这个例子也可以看很出,coroutine.resume其实是个阻塞函数,阻塞等待协同程序完成或者yield退出。可以把协同程序当成一个等待对象,对象等待返回则coroutine.resume返回。在coroutine.resume调用的地方阻塞调用线程,这点很重要。
yield()函数
local co = coroutine.create(function(name)
for i = 1, 2, 1 do
print(name);
print("co:" .. coroutine.yield("yield param"));
end
end);
for i = 1, 2, 1 do
print("=========第" .. i .. "次执行:")
local result, msg = coroutine.resume(co, "resume param");
print("msg:" .. msg);
end
执行结果:
=========第1次执行:
resume param
msg:yield param
=========第2次执行:
co:resume param
resume param
msg:yield param
第一次执行的时候,协同程序第一次被挂起,所以yield的返回要等待第二次resume被调用时才能得到。
于是,第二次调用resume时,首先就得到了上一次yield的返回值了,这个返回值正是resume的第二个参数。
从这个例子可以看出,如果resume()是第一次启动协同程序,那么它的第二个参数会传递给该协同程序的内置函数,作为其参数使用。如果是将被挂起的协同程序重新执行,则resume()的第二个参数,会作为yield()函数的返回值传递给该内置函数。
协同程序内置函数的返回值
local co = coroutine.create(function(name)
for i = 1, 2, 1 do
print(name);
print("co:" .. coroutine.yield("yield param"));
end
return "协同程序函数结束喇!"
end);
for i = 1, 3, 1 do
print("=========第" .. i .. "次执行:")
local result, msg = coroutine.resume(co, "resume param");
print("msg:" .. msg);
end
执行结果:
=========第1次执行:
resume param
msg:yield param
=========第2次执行:
co: resume param
resume param
msg:yield param
=========第3次执行:
co:resume param
msg:协同程序函数结束喇!
将上面的例子修改下,最后一次resume()后,不让协同程序挂起,在其内置函数执行完成后,如果未再挂起,内置函数的返回值会作为resume()函数的第二个返回值进行传递。
1、resume()函数第一个参数是协同程序,第一个返回值是ture或是false,表示启动协同程序的结果。
2、如果resume()函数是第一次启动协同程序的话,会把第二个参数作为其内置函数的实参进行传递。
3、如果resume()函数是将协同程序从挂起状态启动的话,会把第二个参数作为yield()函数的返回值进行传递。
4、如果resume()函数启动的协同程序被yield()函数挂起的话,resume()函数的第二个返回值会是yield()函数的参数,没有则第二个返回值为nil。
5、如果resume()函数启动的协同程序未被挂起的话,resume()函数的第二个返回值会是该协同程序内置函数的返回值,没有则第二个返回值为nil。
Lua中一对 resume-yield 就这样交换数据。下面第一个例子 resume,没有相应的 yield,即第一次启动协同程序,resume 把额外的参数传递给协同的主程序。
co = coroutine.create(function (a,b,c)
print("co", a,b,c)
end)
coroutine.resume(co, 1, 2, 3) --> co 1 2 3
resume()函数也可以把参数传递给yield()函数。如:
co = coroutine.create(function (a,b)
coroutine.yield(a + b, a - b)
end)
print(coroutine.resume(co, 20, 10)) --> true 30 10
该例子中,resume()函数分别将实参20、10传递给了形参a、b。yield()函数根据a、b的值。yield()函数的两个参数,作为resume()函数的第二、第三返回值。
使用实例
我们尝试使用协同程序完成生产者--消费者问题。
local newProductor
function productor()
local i = 0
for j = 0, 10 do
i = i + 1
print("productor: " .. i)
send(i) -- 将生产的物品发送给消费者
end
end
function consumer()
for j = 0, 10 do
local i = receive() -- 从生产者那里得到物品
print("consumer: " .. i)
end
end
function receive()
local status, value = coroutine.resume(newProductor)
return value
end
function send(x)
coroutine.yield(x) -- x表示需要发送的值,值返回以后,就挂起该协同程序
end
-- 启动程序
newProductor = coroutine.create(productor)
consumer()
productor: 1
consumer: 1
productor: 2
consumer: 2
productor: 3
consumer: 3
productor: 4
consumer: 4
productor: 5
consumer: 5
productor: 6
consumer: 6
productor: 7
consumer: 7
productor: 8
consumer: 8
productor: 9
consumer: 9
productor: 10
consumer: 10
productor: 11
consumer: 11
1、首先创建协同程序的内置函数productor,它负责产生新的i值,然后使用封装的yield()函数为send(),挂起自身,等待其他函数处理变量i。
2、创建消费函数customer(),该函数主要封装了resume()。
3、创建协同程序newProductor,调用customer(),使协同程序开始执行。
4、productor()函数开始执行,i值更新,运行到send()函数后,执行函数yield(),productor()被挂起,resume()获得返回值,其中第二个返回值为yield()的参数,即更新后的i值。
5、customer()函数继续循环,运行到receive()后,执行resume()函数。
6、协同程序继续执行,i值更新,继续运行到send()函数,重复4、5,直到productor()内的循环条件不成立,协同程序执行完毕,被销毁。
用作迭代器的协同程序
我们可以将循环的迭代器看作生产者-消费者模式的特殊的例子。迭代函数产生值给 循环体消费。所以可以使用协同来实现迭代器。协同的一个关键特征是它可以不断颠倒调用者与被调用者之间的关系,这样我们毫无顾虑的使用它实现一个迭代器,而不用保 存迭代函数返回的状态。
我们来完成一个打印一个数组元素的所有的排列来阐明这种应用。直接写这样一个 迭代函数来完成这个任务并不容易,但是写一个生成所有排列的递归函数并不难。思路是这样的:将数组中的每一个元素放到最后,依次递归生成所有剩余元素的排列。代码 如下:
function permgen (a, n)
if n == 0 then
printResult(a)
else
for i=1,n do
-- put i-thelement as thelast one
a[n], a[i] = a[i],a[n]
--generateall permutations of the other elements
permgen(a, n - 1)
-- restore i-thelement
a[n], a[i] = a[i],a[n]
end
end
end
function printResult (a)
for i,v in ipairs(a) do
io.write(v," ")
end
io.write("\n")
end
permgen ({1,2,3,4}, 4)
1、printResult 改为 yield
2、定义一个迭代工厂,修改生成器在生成器内创建迭代函数,并使生 成器运行在一个协同程序内。迭代函数负责请求协同产生下一个可能的排列。
这样我们就可以使用 for 循环来打印出一个数组的所有排列情况了。代码如下:
function permgen (a, n)
if n == 0 then
coroutine.yield(a)
else
for i=1,n do
-- put i-thelement as thelast one
a[n], a[i] = a[i],a[n]
--generateall permutations of the other elements
permgen(a, n - 1)
-- restore i-thelement
a[n], a[i] = a[i],a[n]
end
end
end
function perm (a)
local n = #a
local co = coroutine.create(function()
permgen(a,n)
end)
return function () -- iterator
local code, res =coroutine.resume(co)
return res
end
end
function printResult (a)
for i,v in ipairs(a) do
io.write(v," ")
end
io.write("\n")
end
for p in perm {"a","b", "c"} do
printResult(p)
end