Lua极简入门(九)——协同程序
Lua的协同程序和常见的线程相似,可以具有独立的执行流程,包括所需的数据和内存。
print(coroutine.create(function () end))
-->> thread: 0000000000706a88
该示例创建了一个协同程序,并将其打印出来,显示为thread...
,从这里的显示名看,协调程序确实一个线程。但协调程序和线程相比,也有一些区别;比如Java的多线程,多个线程是可以独立运行,但Lua的协同程序,任意时刻只能运行一个。
同线程类似,协同程序也有状态,其状态及各个状态的含义参见下表。
状态 | 描述 |
---|---|
suspended | 挂起,创建时,状态为挂起 |
running | 运行 |
dead | 死亡,该状态的协同,不能够被再次调起 |
normal | 正常,协同A唤起协调B,协调B处于运行状态,此时协调A状态为normal
|
使用协同时,需要使用coroutine
对象,该对象提供了一些方法用来构建协调程序,该对象共包含7个方法,比较常用的如下所示。
函数名 | 参数 | 返回值 | 描述 |
---|---|---|---|
coroutine.create(f) | function,协同程序执行的主函数,一般为匿名函数 | 协同程序的控制对象,可以理解为线程对象 | 创建一个新的协同程序,参数描述了协同程序的主要工作流程。 |
coroutine.resume(co, val1, ...) | 协同对象,多个传入参数,变长 | boolean [yield参数|return数据]两种返回,具体描述参见下面章节 | 启动一个协同程序。根据是否新创建协同和是否由yield挂起,执行方式不同,如果由yield挂起,则resume从挂起位置开始执行 |
coroutine.status(co) | 协同对象 | 协同程序的状态 | 查询协同程序的状态,分为四种。 |
coroutine.yield(...) | 变长参数 | 再次resume该协同程序的方法,传入的参数 | 挂起正在执行的协同程序 |
- 创建协同程序
co = coroutine.create(
function ()
print("hello coroutine")
end
)
print(co)
-->> thread: 00000000007b88b8
使用coroutine.create
方法创建了一个新的携程,其中function
采用了匿名方法的形式,该函数描述了该携程的主要工作,本例只打印了一个hello
。在实际工作中,采用面向对象概念设计程序时,协同和主流程可以分开书写,function可以以其他类方法引入。
-- 创建Person对象,并提供了考试及批卷,返回得分的方法
local Person = {}
Person.exam = function(score)
print("考试得分:" .. score)
end
-- 考试流程中,创建协同程序
co = coroutine.create(Person.exam)
coroutine.resume(co, 50)
-->> 考试得分:50
- 协同状态
co = coroutine.create(
function ()
print("hello coroutine")
end
)
print(coroutine.status(co))
-->> suspended
使用coroutine.status
方法获取一个协同的状态,本例中因为只创建了一个协同对象,其为挂起状态(suspended),协同有多个状态,如果要展示其四种状态,需要配合其他方法共同完成,会在本节最后的组合中展示这些状态。
- 协同恢复
使用coroutine.resume
方法启动或者再次启动一个协同。如果协同程序运行过程中,没有任何错误产生,将返回true
,如果协同中包含了挂起yield
方法时,除了true
外,yield
中传入的所有参数也都将随 resume
函数一同返回,这个特性很重要,可以使用resume
+yield
组合在多个协同间传递数据;当发生错误时,除了返回false
外,还将返回错误信息。
co = coroutine.create(
function ()
print("hello coroutine")
end
)
local i = coroutine.resume(co) -- 没有yield,并且程序无错误,返回true
print(i)
-->> hello coroutine
-->> true
当产生错误是,除了false外,代表协同失败,同时还会返回错误信息
co = coroutine.create(
function(a, b)
if b == 0 then
error("除数为0")
end
local result = a / b
print("a / b = " .. result)
end
)
local i, err = coroutine.resume(co, 12, 0)
print(i, err)
-->> false json2lua.lua:15: 除数为0
返回数据,多个协同间传输数据
co = coroutine.create(
function(a, b)
if b == 0 then
error("除数为0")
end
local result = a / b
coroutine.yield("a/b结果", result)
print("计算结束")
end
)
local i, des, result = coroutine.resume(co, 12, 2)
print(i, des, result)
print(coroutine.resume(co))
-->> true a/b结果 6.0
-->> 计算结束
-->> true
- 携程挂起
当一个协同创建后,其状态为挂起,使用resume
可以启动协同程序执行,一旦协同程序开始执行,其他流程都将被挂起,等待执行中的协同程序执行完毕。如果在协同程序执行过程中,需要暂停,等待其他程序执行,之后再恢复并继续执行,那么需要使用到coroutine.yield
,该函数可以将当前的协同程序挂起,同时也可以借助yield
与外部进行数据通信。
co = coroutine.create(
function()
for i = 1, 2 do
print(i)
coroutine.yield()
end
end
)
coroutine.resume(co)
-->> 1
coroutine.resume(co)
-->> 2
coroutine.resume(co)
-- 此处什么都不打印,因为循环到2时,挂起,再恢复,跳出循环,此时程序执行完毕
print(coroutine.resume(co)) -- 协同结束,状态为dead,无法再恢复一个死亡的协同,输出错误信息
-->> false cannot resume dead coroutine
这个示例演示了协同的挂起,借助resume
+yield
可以实现更多的功能,在上述的举例中,挂起貌似没有起到太大作用,在这里从另外一个角度思考:如果要下载一个文档,一个协同负责下载和保存文档的任务,并且该协同无法操作UI,直接执行协同,在文件比较大时,可能需要花费许多时间,用户傻傻的等待,可能不知所措,也可能认为程序宕机了,会直接关闭;此时可以借助恢复+挂起机制,实现一个主干线程绘制UI,显示下载的进度。
-- 下载文件服务对象
local FileServer = {}
-- 文档下载
FileServer.download = function()
-- 文档从1%下载到100%
local time = os.time()
local i = 1
local last = 10
local downSuccess = false
while true do
-- 每隔两秒挂起一次,模拟下载
if os.time() - time >= 2 then
if i == last then
downSuccess = true
end
-- 进度提升10倍
local tip = "下载进度:" .. math.modf(i * 10) .. "%"
i = i + 1
time = os.time()
coroutine.yield(tip, downSuccess)
end
if i > last then
break
end
end
end
-- 主干流程
local downloadCo = coroutine.create(FileServer.download)
local uiCo = coroutine.create(
function()
local ok = false
repeat
local _, tip, ok = coroutine.resume(downloadCo)
print(tip)
if ok then
print("下载完成")
end
until ok
end
)
-- 点击下载按钮执行下载
coroutine.resume(uiCo)
-->> 下载进度:10%
-->> 下载进度:20%
-->> ...
-->> 下载进度:100%
-->> 下载完成
在本例的下载示例中,其核心思想是下载服务中,只完成http下载文件及保存文件行为,并每隔2秒往外部传递当前进度数据,UI控制协同程序接收到数据后,刷新UI显示。使用os.time
函数进行间隔挂起协同。
- 方法组合
在之前的示例中,并没有看到协同的四种状态,挂起状态很简单,当创建一个协同程序时,其状态就是挂起;运行状态也比较容易理解,当一个协调程序被启动后,其状态为运行;当一个协同程序运行完毕后,其状态为死亡,之前也看到过。
这里比较难于理解的是normal正常
状态,按文档描述:当有两个协同程序时,协同程序A在执行过程中,启动协同程序B,此时协同程序B状态为运行状态,而协同程序A此时既不是运行、也不是挂起,是一种特殊的状态,即为正常状态。
co = coroutine.create(
function()
print("corotine A")
-- 此时协同co2启动协同co,co处于运行状态,co2处于正常状态
print("co2 status:", coroutine.status(co2))
coroutine.yield()
print("corotine A continue run")
end
)
co2 = coroutine.create(
function()
print("co2 status:", coroutine.status(co2))
coroutine.resume(co)
coroutine.resume(co)
end
)
print("co status:", coroutine.status(co)) -- 挂起
coroutine.resume(co2)
-- 此时协同co2、co都运行结束,状态为死亡
print("co2 status:", coroutine.status(co2))
-- 上述程序输出
-->> co status: suspended
-->> co2 status: running
-->> corotine A
-->> co2 status: normal
-->> corotine A continue run
-->> co2 status: dead
在介绍挂起方法的时候,介绍过resume
+yield
进行数据交换,主要是从协同中往外传递数据,当协同中使用yield
挂起时,方法yield
也会有返回值,其返回值,为外部调用resume
的传入参数,这种方式可以实现在协同程序运行期间,不断的进行数据交换。
local co = coroutine.create(
function(a, b)
print(a, b) -- 1. 输出 4,5
local c, d = coroutine.yield(a + 1, b - 1) -- 2. 挂起,向外传递数据 5,4
print(a, b) -- 4. 继续执行,输出4,5
print(c, d) -- 5. yield返回的数据为上一个resume的参数,输出 8,9
return c + d -- 6. 返回 17
end
)
print(coroutine.resume(co, 4, 5)) -- 3. 输出 true 5 4
print(coroutine.resume(co, 8, 9)) -- 7. 输出 true 17
-->> 4 5
-->> true 5 4
-->> 4 5
-->> 8 9
-->> true 17
这个例子介绍了resume
+yield
最后一种传递数据的方式,理解了这些,对于理解协同程序有很大帮助,正是因为Lua协同程序的这些灵活的特点,可以开发出很多特性的功能,前提是需要对这些特性理解清楚,因为灵活的另一面就是难于理解。理解这些,终于可以理解Lua官方文档中关于协同程序一章的示例。
function foo (a)
print("foo", a) -- 2. foo 2
return coroutine.yield(2 * a) -- 3. 挂起 6. resume传入参数"r",作为yield的返回,并作为foo的返回
end
co = coroutine.create(function(a, b)
print("co-body", a, b) -- 1. co-body 1 10
local r = foo(a + 1) -- 7. r = "r"
print("co-body", r) -- 8. co-body r
local r, s = coroutine.yield(a + b, a - b) -- 9. 挂起, 传出 11 -9 12. yield返回 r="x",s="y"
print("co-body", r, s) -- 13. co-body x y
return b, "end"
end)
print("main", coroutine.resume(co, 1, 10)) -- 4. main true 4
print("main", coroutine.resume(co, "r")) -- 5. 启动,并传入 "r" -- 10. main true 11 -9
print("main", coroutine.resume(co, "x", "y")) -- 11. 启动,传入 "x" "y" 14. main true 10 end
print("main", coroutine.resume(co, "x", "y")) -- main false error
-->> co-body 1 10
-->> foo 2
-->> main true 4
-->> co-body r
-->> main true 11 -9
-->> co-body x y
-->> main true 10 end
-->> main false cannot resume dead coroutine
启动协同程序时,resume至少携带一个参数(协同),当其携带多个参数时,
a. 当协同程序刚创建并处于挂起状态时,此时resume的参数将作为协同函数的参数传入
b. 当协同程序由yield挂起时,resume的参数将作为yield函数的返回值传入协同程序
resume返回值情况
a. 当协同程序由yield挂起时,此时返回值格式为
boolean [yield函数参数]
,布尔值表示当前协同程 序执行的状态,true为无错误,yield函数的参数将作为返回值传出,可以使用该类型往主程序传递 数据 b. 当协同程序结束时,返回值格式为
boolean [return数据]
,布尔值表示协同程序执行状态;return 数据为协同方法的返回值,如果没有返回值,则值具有布尔值。