Lua 协同程序(coroutine)
什么是协同(coroutine)?
Lua 协同程序(coroutine)与线程比较类似:拥有独立的堆栈,独立的局部变量,独立的指令指针,同时又与其它协同程序共享全局变量和其它大部分东西。
协同程序可以理解为一种特殊的线程,可以暂停和恢复其执行,从而允许非抢占式的多任务处理。
协同是非常强大的功能,但是用起来也很复杂。
基本语法
协同程序由 coroutine 模块提供支持。2>
使用协同程序,你可以在函数中使用 coroutine.create 创建一个新的协同程序对象,并使用 coroutine.resume 启动它的执行。协同程序可以通过调用 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的时候,就是返回一个 coroutine 的线程号 |
以下实例演示了如何使用 Lua 协同程序:
实例
print("协同程序 foo 开始执行")
local value = coroutine.yield("暂停 foo 的执行")
print("协同程序 foo 恢复执行,传入的值为: " .. tostring(value))
print("协同程序 foo 结束执行")
end
-- 创建协同程序
local co = coroutine.create(foo)
-- 启动协同程序
local status, result = coroutine.resume(co)
print(result) -- 输出: 暂停 foo 的执行
-- 恢复协同程序的执行,并传入一个值
status, result = coroutine.resume(co, 42)
print(result) -- 输出: 协同程序 foo 恢复执行,传入的值为: 42
以上实例中,我们定义了一个名为 foo 的函数作为协同程序。在函数中,我们使用 coroutine.yield 暂停了协同程序的执行,并返回了一个值
。在主程序中,我们使用 coroutine.create 创建了一个协同程序对象,并使用 coroutine.resume 启动了它的执行。
在第一次调用 coroutine.resume 后,协同程序执行到 coroutine.yield 处暂停,并将值返回给主程序。然后,我们再次调用 coroutine.resume,并传入一个值作为协同程序恢复执行时的参数。
执行以上代码输出结果为:
协同程序 foo 开始执行 暂停 foo 的执行 协同程序 foo 恢复执行,传入的值为: 42 协同程序 foo 结束执行 nil
需要注意的是,协同程序的状态可以通过 coroutine.status 函数获取,通过检查状态可以确定协同程序的执行情况(如运行中、已挂起、已结束等)。
以下实例演示了以上各个方法的用法:
coroutine_test.lua 文件
-- 创建了一个新的协同程序对象 co,其中协同程序函数打印传入的参数 i
co = coroutine.create(
function(i)
print(i);
end
)
-- 使用 coroutine.resume 启动协同程序 co 的执行,并传入参数 1。协同程序开始执行,打印输出为 1
coroutine.resume(co, 1) -- 1
-- 通过 coroutine.status 检查协同程序 co 的状态,输出为 dead,表示协同程序已经执行完毕
print(coroutine.status(co)) -- dead
print("----------")
-- 使用 coroutine.wrap 创建了一个协同程序包装器,将协同程序函数转换为一个可直接调用的函数对象
co = coroutine.wrap(
function(i)
print(i);
end
)
co(1)
print("----------")
-- 创建了另一个协同程序对象 co2,其中的协同程序函数通过循环打印数字 1 到 10,在循环到 3 的时候输出当前协同程序的状态和正在运行的线程
co2 = coroutine.create(
function()
for i=1,10 do
print(i)
if i == 3 then
print(coroutine.status(co2)) --running
print(coroutine.running()) --thread:XXXXXX
end
coroutine.yield()
end
end
)
-- 连续调用 coroutine.resume 启动协同程序 co2 的执行
coroutine.resume(co2) --1
coroutine.resume(co2) --2
coroutine.resume(co2) --3
-- 通过 coroutine.status 检查协同程序 co2 的状态,输出为 suspended,表示协同程序暂停执行
print(coroutine.status(co2)) -- suspended
print(coroutine.running())
print("----------")
以上实例执行输出结果为:
1 dead ---------- 1 ---------- 1 2 3 running thread: 0x7fb801c05868 false suspended thread: 0x7fb801c04c88 true ----------
coroutine.running就可以看出来,coroutine在底层实现就是一个线程。
当create一个coroutine的时候就是在新线程中注册了一个事件。
当使用resume触发事件的时候,create的coroutine函数就被执行了,当遇到yield的时候就代表挂起当前线程,等候再次resume触发事件。
接下来我们分析一个更详细的实例:
实例
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("---分割线---")
以上实例执行输出结果为:
第一次协同程序执行输出 1 10 foo 函数输出 2 main true 4 --分割线---- 第二次协同程序执行输出 r main true 11 -9 ---分割线--- 第三次协同程序执行输出 x y main true 10 结束协同程序 ---分割线--- main false cannot resume dead coroutine ---分割线---
以上实例接下如下:
- 调用resume,将协同程序唤醒,resume操作成功返回true,否则返回false;
- 协同程序运行;
- 运行到yield语句;
- yield挂起协同程序,第一次resume返回;(注意:此处yield返回,参数是resume的参数)
- 第二次resume,再次唤醒协同程序;(注意:此处resume的参数中,除了第一个参数,剩下的参数将作为yield的参数)
- yield返回;
- 协同程序继续运行;
- 如果使用的协同程序继续运行完成后继续调用 resume方法则输出:cannot resume dead coroutine
resume和yield的配合强大之处在于,resume处于主程中,它将外部状态(数据)传入到协同程序内部;而yield则将内部的状态(数据)返回到主程中。
生产者-消费者问题
现在我就使用Lua的协同程序来完成生产者-消费者这一经典问题。
实例
function productor()
local i = 0
while true do
i = i + 1
send(i) -- 将生产的物品发送给消费者
end
end
function consumer()
while true do
local i = receive() -- 从生产者那里得到物品
print(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()
以上实例执行输出结果为:
1 2 3 4 5 6 7 8 9 10 11 12 13 ……
线程和协同程序区别
线程与协同程序的主要区别在于,一个具有多个线程的程序可以同时运行几个线程,而协同程序却需要彼此协作的运行。
在任一指定时刻只有一个协同程序在运行,并且这个正在运行的协同程序只有在明确的被要求挂起的时候才会被挂起。
协同程序有点类似同步的多线程,在等待同一个线程锁的几个线程有点类似协同。
主要区别归纳如下:
调度方式:线程通常由操作系统的调度器进行抢占式调度,操作系统会在不同线程之间切换执行权。而协同程序是非抢占式调度的,它们由程序员显式地控制执行权的转移。
并发性:线程是并发执行的,多个线程可以同时运行在多个处理器核心上,或者通过时间片轮转在单个核心上切换执行。协同程序则是协作式的,只有一个协同程序处于运行状态,其他协同程序必须等待当前协同程序主动放弃执行权。
内存占用:线程通常需要独立的堆栈和上下文环境,因此线程的创建和销毁会带来额外的开销。而协同程序可以共享相同的堆栈和上下文,因此创建和销毁协同程序的开销较小。
数据共享:线程之间可以共享内存空间,但需要注意线程安全性和同步问题。协同程序通常通过参数传递和返回值来进行数据共享,不同协同程序之间的数据隔离性较好。
调试和错误处理:线程通常在调试和错误处理方面更复杂,因为多个线程之间的交互和并发执行可能导致难以调试的问题。协同程序则在调试和错误处理方面相对简单,因为它们是由程序员显式地控制执行流程的。
总体而言,线程适用于需要并发执行的场景,例如在多核处理器上利用并行性加快任务的执行速度。而协同程序适用于需要协作和协调的场景,例如状态机、事件驱动编程或协作式任务处理。选择使用线程还是协同程序取决于具体的应用需求和编程模型。
longlongago
lon***500@163.com
这一章的例子较难理解,如果把yield()和resume()两个函数的行为描述清楚了,就好理解多了。
例子再简化一下:
longlongago
lon***500@163.com
达也酱
jja***@163.com
coroutine.creat方法和coroutine.wrap需要特别注意的是这个返回值的类型,功能上有些类似,但并不完全一样。
coroutine.creat返回的是一个协同程序,类型为thread,需要使用coroutine.resume进行调用;而coroutine.wrap返回的是一个普通的方法(函数),类型为function,和普通function有同样的使用方法,并且不能使用coroutine.resume进行调用。
以下代码进行测试:
输出:
达也酱
jja***@163.com
达也酱
jja***@163.com
coroutine.resume方法需要特别注意的一点是,这个方法只要调用就会返回一个boolean值。
coroutine.resume方法如果调用成功,那么返回true,如果有yield方法,同时返回yield括号里的参数;如果失败,那么返回false,并且带上一句"cannot resume dead coroutine"
以下代码进行测试:
输出:
达也酱
jja***@163.com
达也酱
jja***@163.com
coroutine.creat方法只要建立了一个协程 ,那么这个协程的状态默认就是suspend。使用resume方法启动后,会变成running状态;遇到yield时将状态设为suspend;如果遇到return,那么将协程的状态改为dead。
coroutine.resume方法需要特别注意的一点是,这个方法只要调用就会返回一个boolean值。
coroutine.resume方法如果调用成功,那么返回true;如果有yield方法,同时返回yield括号里的参数;如果没有yield,那么继续运行直到协程结束;直到遇到return,将协程的状态改为dead,并同时返回return的值。
coroutine.resume方法如果调用失败(调用状态为dead的协程会导致失败),那么返回false,并且带上一句"cannot resume dead coroutine"
以下代码进行测试:
输出:
达也酱
jja***@163.com
大轩
tol***uan@163.com
挂起协程: yield 除了挂起协程外,还可以同时返回数据给 resume ,并且还可以同时定义下一次唤醒时需要传递的参数。
执行结果(结果中 true 表示本次调用成功):
大轩
tol***uan@163.com
老K
908***652@qq.com
正文中的例子分析:
第一次调用(第一次调用的时候,协同程序是一个挂起的状态),resume 的参数 1,10 传入主体函数,打印得出 1,10,之后调用 foo 打印得出 2,程序挂起,之后返回这个值到 resume,作为第二个参数值为 4。
第二次调用 resume 参数为 r ,从主函数中 print("第二次协同程序执行输出", r) 开始运行,因为此时的状态是挂起的状态,resume 的参数传入 yield,作为挂起点的返回值为 r。
所以打印得出 r ,之后继续运行,执行 local r, s = coroutine.yield(a + b, a - b),因为此时,并非 resume 直接调用的情况,所以 yield 函数 使用主函数传入的 a,b 参数 作为参数,得出结果为 11, -9,之后 再次挂起。
结果: r 11 -9
第三次调用 resume 参数为 x, y ,从程序挂起点运行,并参数传入 yield 中,yield 此时作为返回值点,所以得出 r,s 结果为 x , y,之后继续运行 return b, "结束协同程序" , 返回 b, 为 10。
结果: x,y true, 10
总结: resume 执行的情况如果(排除第一次执行情况)是挂起的状态,那么 resume 的参数传递给 yield,yield 不论参数表达式形式,返回的值 resume 传递的所有参数。
特别的,注意如果运行之后,再次挂起,那么此时传入的 yield 值,就是主函数的参数值,如果使用的话。
如果 resume 的执行是第一次(上面讲到排除第一次挂起的特殊情况)的情况或者是挂起之后再次运行,那么 resume 的参数 作为主函数的参数。
老K
908***652@qq.com