Lua 协同程序(coroutine)
参考文章:
菜鸟教程
https://zhuanlan.zhihu.com/p/480357405
https://zhuanlan.zhihu.com/p/76249973
Lua 协同程序(coroutine)与线程比较类似:拥有独立的堆栈,独立的局部变量,独立的指令指针,同时又与其它协同程序共享全局变量和其它大部分东西。
协同有非常强大的功能,但是用起来也很复杂。
线程和协同程序的区别
线程与协同程序的主要区别在于,一个具有多个线程的程序可以同时运行几个线程,而协同程序却需要彼此协作的运行。在任一指定时刻只有一个协同程序在运行,并且这个正在运行的协同程序只有在明确的被要求挂起(yield)的时候才会被挂起。
协同程序有点类似于同步的多线程,在等待一个线程锁的几个线程有点类似协同。
基础语法
方法 | 描述 |
---|---|
coroutine.create() | 创建coroutine,返回coroutine对象,参数为一个匿名函数或函数,当和 resume 配合使用的时候就唤醒函数调用。 |
coroutine.resume() | 重启 coroutine,和 create 配合使用。 |
coroutine.yield() | 挂起 coroutine,将 coroutine 设置为挂起状态,这个和 resume 配合使用能有很多有用的效果。 |
coroutine.status() | 查看 coroutine 的状态。 注:coroutine 的状态有三种:dead,suspended,running,具体什么时候有这样的状态请参考下面的程序。 |
coroutine.wrap() | 创建 coroutine,返回一个函数,一旦你调用这个函数,就进入 coroutine,和 create 功能重复。 |
coroutine.running() | 返回正在运行的 coroutine,一个 coroutine 就类似于一个线程,当使用running的时候,就是返回一个 corouting 的线程号。 如果是主协程,返回的结果后面会跟一个true,表示当前的调用coroutine.running()的区域位于主协程中,否则则为false。 |
TIPS:
上述提到的coroutine不仅仅指create出来的coroutine,同时包括默认的主coroutine。
主协程是指Lua的默认执行线,用coroutine.create创建的协程不是主协程。
协程的基础操作
co = coroutine.create(function()
print("123")
end
)
coroutine.resume(co) --运行协程,打印123
新创建的协程并不会运行,而是处于挂起状态,可以通过coroutine.resume
让其运行。这是因为Lua提供的是非对称协程(asymmertric coroutine
),也就是说需要两个函数来控制协程的运行,一个用于挂起协程的执行,另一个用于恢复它的执行。
以下实例演示以上各个方法的用法:
co = coroutine.create(
function(i)
print(i);
end
)
coroutine.resume(co, 1) -- 1
print(coroutine.status(co)) -- dead
print("----------")
co = coroutine.wrap(
function(i)
print(i);
end
)
co(1)
print("----------")
co2 = coroutine.create(
function()
for i=1,10 do
print(i)
if i == 3 or i == 2 then
print(coroutine.status(co2)) --running
print(coroutine.running()) --thread:XXXXXX false表示在协程中,true表示在主协程中
end
coroutine.yield()
end
end
)
coroutine.resume(co2) --1
coroutine.resume(co2) --2
coroutine.resume(co2) --3
print(coroutine.status(co2)) -- suspended
print(coroutine.running())
输出结果:
coroutine.running就可以看出来,coroutine在底层实现就是一个线程。当create一个coroutine的时候就在一个新线程中注册一个事件。当使用resume触发事件的时候,create的coroutine函数就被执行了,当遇到yield的时候就代表挂起当前线程,等待再次resume触发事件。
接下来分析一个更详细的实例:
function foo (a)
print("foo 函数输出", a)
return coroutine.yield(2 * a) -- 返回 2*a 的值
end
co = coroutine.create(function (a , b)
print("第一次协同程序执行输出", a, b) -- co-body 1 10
local r = foo(a + 1)
print("第二次协同程序执行输出", r)
local r, s = coroutine.yield(a + b, a - b) -- a,b的值为第一次调用协同程序时传入
print("第三次协同程序执行输出", r, s)
return b, "结束协同程序" -- b的值为第二次调用协同程序时传入
end)
print("main", coroutine.resume(co, 1, 10)) -- true, 4
print("--分割线----")
print("main", coroutine.resume(co, "r")) -- true 11 -9
print("---分割线---")
print("main", coroutine.resume(co, "x", "y")) -- true 10 end
print("---分割线---")
print("main", coroutine.resume(co, "x", "y")) -- cannot resume dead coroutine
print("---分割线---")
输出结果:
以上实例介绍如下:
- 一开始调用resume,将协同程序唤醒,resume操作成功返回true,否则返回false;
- 子协程第一次启动,输出结果;
- 运行到yield语句;
- yield首先挂起子协程,并将2*a返回给主协程resume(即第17行运行成功打印true的同时,接收到2*a也进行print输出);
- 第二次resume,再次唤醒协同程序(此时因为挂起时已经位于第8行的位置,那么重新启动也从该行启动,此时resume的参数中,除了第一个参数,其它参数都会传递给yield,即作为yield的返回值,那么第8行local r = “r”);
- yield返回;
- 协同程序继续运行(同理);
- 如果使用的协同程序继续运行完成后继续调用 resume方法则输出:cannot resume dead coroutine。
从上述我们可以得出结论:resume和yield的配合强大之处在于resume处于主协程中,它将外部状态(数据)传入到子协程中的;而yield则将内部的状态(数据)返回到主协程中。
我们发现resume被调用后,有两种传递方式,一种是匿名函数的参数进行传递,一种是通过yield的返回值进行传递。
通过上述介绍,我们可以得出以下代码的输出结果的合理性:
co = coroutine.create(function(x)
print(coroutine.yield())
end
)
coroutine.resume(co,1) --不打印任何值
coroutine.resume(co,1,2) --打印 1 2
生产 - 消费者问题
消费者不需要,则生产者就不生产。消费者一有消费需求,生产者立即生产。
local newProductor
function Productor()
local i = 0
while true do
i = i + 1
send(i) -- 将生产的物品发送给消费者
end
end
function Consumer()
while true do
local i = recv() -- 从生产者那里得到物品
print(i)
end
end
function recv()
local status, value = coroutine.resume(newProductor)
-- print(status)
return value
end
function send(x)
coroutine.yield(x) -- x表示需要发送的值,值返回以后,就挂起该协同程序
end
newProductor = coroutine.create(Productor)
Consumer()
输出结果:
输出结果由于消费者一直有需求,生产者是一直在生产的。