5. 协程 Coroutine
最后更新于:2022-04-01 23:21:58
协程(coroutine)并不是 Lua 独有的概念,如果让我用一句话概括,那么大概就是:一种能够在运行途中主动中断,并且能够从中断处恢复运行的特殊函数。(嗯,其实不是函数。)
### [](https://github.com/andycai/luaprimer/blob/master/05.md#举个最原始的例子)举个最原始的例子:
下面给出一个最简单的 Lua 中 coroutine 的用法演示:
~~~
function greet()
print "hello world"
end
co = coroutine.create(greet) -- 创建 coroutine
print(coroutine.status(co)) -- 输出 suspended
print(coroutine.resume(co)) -- 输出 hello world
-- 输出 true (resume 的返回值)
print(coroutine.status(co)) -- 输出 dead
print(coroutine.resume(co)) -- 输出 false cannot resume dead coroutine (resume 的返回值)
print(type(co)) -- 输出 thread
~~~
协程在创建时,需要把协程体函数传递给创建函数 create。新创建的协程处于 suspended 状态,可以使用 resume 让其运行,全部执行完成后协程处于 dead 状态。如果尝试 resume 一个 dead 状态的,则可以从 resume 返回值上看出执行失败。另外你还可以注意到 Lua 中协程(coroutine)的变量类型其实叫做「thread」Orz...
乍一看可能感觉和线程没什么两样,但需要注意的是 resume 函数只有在 greet 函数「返回」后才会返回(所以说协程像函数)。
### [](https://github.com/andycai/luaprimer/blob/master/05.md#函数执行的中断与再开) 函数执行的中断与再开
单从上面这个例子,我们似乎可以得出结论:协程果然就是某种坑爹的函数调用方式啊。然而,协程的真正魅力来自于 resume 和 yield 这对好基友之间的羁绊。
### [](https://github.com/andycai/luaprimer/blob/master/05.md#函数-coroutineresumeco-val1-)函数 coroutine.resume(co[, val1, ...])
开始或恢复执行协程 co。
如果是开始执行,val1 及之后的值都作为参数传递给协程体函数;如果是恢复执行,val1 及之后的值都作为 yield 的返回值传递。
第一个返回值(还记得 Lua 可以返回多个值吗?)为表示执行成功与否的布尔值。如果成功,之后的返回值是 yield 的参数;如果失败,第二个返回值为失败的原因(Lua 的很多函数都采用这种错误处理方式)。
当然,如果是协程体函数执行完毕 return 而不是 yield,那么 resume 第一个返回值后跟着的就是其返回值。
### [](https://github.com/andycai/luaprimer/blob/master/05.md#函数-coroutineyield)函数 coroutine.yield(...)
中断协程的执行,使得开启该协程的 coroutine.resume 返回。再度调用 coroutine.resume 时,会从该 yield 处恢复执行。
当然,yield 的所有参数都会作为 resume 第一个返回值后的返回值返回。
OK,总结一下:当 co = coroutine.create(f) 时,yield 和 resume 的关系如下图:
### [](https://github.com/andycai/luaprimer/blob/master/05.md#how-coroutine-makes-life-easier)How coroutine makes life easier
如果要求给某个怪写一个 AI:先向右走 30 帧,然后只要玩家进入视野就往反方向逃 15 帧。该怎么写?
#### [](https://github.com/andycai/luaprimer/blob/master/05.md#传统做法)传统做法
经典的纯状态机做法。
~~~
-- 每帧的逻辑
function Monster:frame()
self:state_func()
self.state_frame_count = self.state_frame_count + 1
end
-- 切换状态
function Monster:set_next_state(state)
self.state_func = state
self.state_frame_count = 0
end
-- 首先向右走 30 帧
function Monster:state_walk_1()
local frame = self.state_frame_count
self:walk(DIRECTION_RIGHT)
if frame > 30 then
self:set_next_state(state_wait_for_player)
end
end
-- 等待玩家进入视野
function Monster:state_wait_for_player()
if self:get_distance(player) < self.range then
self.direction = -self:get_direction_to(player)
self:set_next_state(state_walk_2)
end
end
-- 向反方向走 15 帧
function Monster:state_walk_2()
local frame = self.state_frame_count;
self:walk(self.direction)
if frame > 15 then
self:set_next_state(state_wait_for_player)
end
end
~~~
#### [](https://github.com/andycai/luaprimer/blob/master/05.md#协程做法)协程做法
~~~
-- 每帧的逻辑
function Monster:frame()
-- 首先向右走 30 帧
for i = 1, 30 do
self:walk(DIRECTION_RIGHT)
self:wait()
end
while true do
-- 等待玩家进入视野
while self:get_distance(player) >= self.range do
self:wait()
end
-- 向反方向走 15 帧
self.direction = -self:get_direction_to(player)
for i = 1, 15 do
self:walk(self.direction)
self:wait()
end
end
end
-- 该帧结束
function Monster:wait()
coroutine.yield()
end
~~~
额外说一句,从 wait 函数可以看出,Lua 的协程并不要求一定要从协程体函数中调用 yield,这是和 Python 的一个区别。
协同程序(coroutine,这里简称协程)是一种类似于线程(thread)的东西,它拥有自己独立的栈、局部变量和指令指针,可以跟其他协程共享全局变量和其他一些数据,并且具有一种挂起(yield)中断协程主函数运行,下一次激活恢复协程会在上一次中断的地方继续执行(resume)协程主函数的控制机制。
Lua 把关于协程的所有函数放在一个名为 “coroutine” 的 table 里,coroutine 里具有以下几个内置函数:
~~~
-coroutine-yield [function: builtin#34]
| -wrap [function: builtin#37]
| -status [function: builtin#31]
| -resume [function: builtin#35]
| -running [function: builtin#32]
| -create [function: builtin#33]
~~~
### [](https://github.com/andycai/luaprimer/blob/master/05.md#coroutinecreate---创建协程)coroutine.create - 创建协程
函数 coroutine.create 用于创建一个新的协程,它只有一个以函数形式传入的参数,该函数是协程的主函数,它的代码是协程所需执行的内容
~~~
co = coroutine.create(function()
io.write("coroutine create!\n")
end)
print(co)
~~~
当创建完一个协程后,会返回一个类型为 thread 的对象,但并不会马上启动运行协程主函数,协程的初始状态是处于挂起状态
### [](https://github.com/andycai/luaprimer/blob/master/05.md#coroutinestatus---查看协程状态)coroutine.status - 查看协程状态
协程有 4 种状态,分别是:挂起(suspended)、运行(running)、死亡(dead)和正常(normal),可以通过 coroutine.status 来输出查看协程当前的状态。
~~~
print(coroutine.status(co))
~~~
### [](https://github.com/andycai/luaprimer/blob/master/05.md#coroutineresume---执行协程)coroutine.resume - 执行协程
函数 coroutine.resume 用于启动或再次启动一个协程的执行
~~~
coroutine.resume(co)
~~~
协程被调用执行后,其状态会由挂起(suspended)改为运行(running)。不过当协程主函数全部运行完之后,它就变为死亡(dead)状态。
传递给 resume 的额外参数都被看作是协程主函数的参数
~~~
co = coroutine.create(function(a, b, c)
print("co", a, b, c)
end)
coroutine.resume(co, 1, 2, 3)
~~~
协程主函数执行完时,它的主函数所返回的值都将作为对应 resume 的返回值
~~~
co = coroutine.create(function()
return 3, 4
end)
print(coroutine.resume(co))
~~~
### [](https://github.com/andycai/luaprimer/blob/master/05.md#coroutineyield---中断协程运行)coroutine.yield - 中断协程运行
coroutine.yield 函数可以让一个运行中的协程中断挂起
~~~
co = coroutine.create(function()
for i = 1, 3 do
print("before coroutine yield", i)
coroutine.yield()
print("after coroutine yield", i)
end
end)
coroutine.resume(co)
~~~
coroutine.resume(co) 上面第一个 resume 唤醒执行协程主函数代码,直到第一个 yield。第二个 resume 激活被挂起的协程,并从上一次协程被中断 yield 的位置继续执行协程主函数代码,直到再次遇到 yield 或程序结束。
resume 执行完协程主函数或者中途被挂起(yield)时,会有返回值返回,第一个值是 true,表示执行没有错误。如果是被 yield 挂起暂停,yield 函数有参数传入的话,这些参数会接着第一个值后面一并返回
~~~
co = coroutine.create(function(a, b, c)
coroutine.yield(a, b, c)
end)
print(coroutine.resume(co, 1, 2, 3))
~~~
### [](https://github.com/andycai/luaprimer/blob/master/05.md#以-coroutinewrap-的方式创建协程)以 coroutine.wrap 的方式创建协程
跟 coroutine.create 一样,函数 coroutine.wrap 也是创建一个协程,但是它并不返回一个类型为 thread 的对象,而是返回一个函数。每当调用这个返回函数,都会执行协程主函数运行。所有传入这个函数的参数等同于传入 coroutine.resume 的参数。 coroutine.wrap 会返回所有应该由除第一个(错误代码的那个布尔量) 之外的由 coroutine.resume 返回的值。 和 coroutine.resume 不同之处在于, coroutine.wrap 不会返回错误代码,无法检测出运行时的错误,也无法检查 wrap 所创建的协程的状态
~~~
function wrap(param)
print("Before yield", param)
obtain = coroutine.yield()
print("After yield", obtain)
return 3
end
resumer = coroutine.wrap(wrap)
print(resumer(1))
print(resumer(2))
~~~
### [](https://github.com/andycai/luaprimer/blob/master/05.md#coroutinerunning---返回正在运行中的协程)coroutine.running - 返回正在运行中的协程
函数 coroutine.running 用于返回正在运行中的协程,如果没有协程运行,则返回 nil
~~~
print(coroutine.running())
co = coroutine.create(function()
print(coroutine.running())
print(coroutine.running() == co)
end)
coroutine.resume(co)
print(coroutine.running())
~~~
### [](https://github.com/andycai/luaprimer/blob/master/05.md#resume-yield-交互)resume-yield 交互
下面代码放在一个 lua 文件里运行,随便输入一些字符后按回车,则会返回输出刚才输入的内容
~~~
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()
send(x)
end
end)
end
function filter(prod)
return coroutine.create(function()
-- for line = 1, math.huge do
for line = 1, 5 do
local x = receive(prod)
x = string.format("%5d Enter is %s", line, x)
send(x)
end
end)
end
function consumer(prod)
-- repeat
-- local x = receive(prod)
-- print(type(x))
-- if x then
-- io.write(x, "\n")
-- end
-- until x == nil
while true do
local obtain = receive(prod)
if obtain then
io.write(obtain, "\n\n")
else
break
end
end
end
p = producer()
f = filter(p)
consumer(f)
~~~
';