协程 Coroutine

协程(coroutine)并不是 Lua 独有的概念,如果让我用一句话概括,那么大概就是:一种能够在运行途中主动中断,并且能够从中断处恢复运行的特殊函数。(嗯,其实不是函数。)

举个最原始的例子:

下面给出一个最简单的 Lua 中 coroutine 的用法演示:

  1. function greet()
  2. print "hello world"
  3. end
  4. co = coroutine.create(greet) -- 创建 coroutine
  5. print(coroutine.status(co)) -- 输出 suspended
  6. print(coroutine.resume(co)) -- 输出 hello world
  7. -- 输出 true (resume 的返回值)
  8. print(coroutine.status(co)) -- 输出 dead
  9. print(coroutine.resume(co)) -- 输出 false cannot resume dead coroutine (resume 的返回值)
  10. print(type(co)) -- 输出 thread

协程在创建时,需要把协程体函数传递给创建函数 create。新创建的协程处于 suspended 状态,可以使用 resume 让其运行,全部执行完成后协程处于 dead 状态。如果尝试 resume 一个 dead 状态的,则可以从 resume 返回值上看出执行失败。另外你还可以注意到 Lua 中协程(coroutine)的变量类型其实叫做「thread」Orz…

乍一看可能感觉和线程没什么两样,但需要注意的是 resume 函数只有在 greet 函数「返回」后才会返回(所以说协程像函数)。

 函数执行的中断与再开

单从上面这个例子,我们似乎可以得出结论:协程果然就是某种坑爹的函数调用方式啊。然而,协程的真正魅力来自于 resume 和 yield 这对好基友之间的羁绊。

函数 coroutine.resume(co[, val1, …])

开始或恢复执行协程 co。

如果是开始执行,val1 及之后的值都作为参数传递给协程体函数;如果是恢复执行,val1 及之后的值都作为 yield 的返回值传递。

第一个返回值(还记得 Lua 可以返回多个值吗?)为表示执行成功与否的布尔值。如果成功,之后的返回值是 yield 的参数;如果失败,第二个返回值为失败的原因(Lua 的很多函数都采用这种错误处理方式)。

当然,如果是协程体函数执行完毕 return 而不是 yield,那么 resume 第一个返回值后跟着的就是其返回值。

函数 coroutine.yield(…)

中断协程的执行,使得开启该协程的 coroutine.resume 返回。再度调用 coroutine.resume 时,会从该 yield 处恢复执行。

当然,yield 的所有参数都会作为 resume 第一个返回值后的返回值返回。

OK,总结一下:当 co = coroutine.create(f) 时,yield 和 resume 的关系如下图:

How coroutine makes life easier

如果要求给某个怪写一个 AI:先向右走 30 帧,然后只要玩家进入视野就往反方向逃 15 帧。该怎么写?

传统做法

经典的纯状态机做法。

  1. -- 每帧的逻辑
  2. function Monster:frame()
  3. self:state_func()
  4. self.state_frame_count = self.state_frame_count + 1
  5. end
  6. -- 切换状态
  7. function Monster:set_next_state(state)
  8. self.state_func = state
  9. self.state_frame_count = 0
  10. end
  11. -- 首先向右走 30
  12. function Monster:state_walk_1()
  13. local frame = self.state_frame_count
  14. self:walk(DIRECTION_RIGHT)
  15. if frame > 30 then
  16. self:set_next_state(state_wait_for_player)
  17. end
  18. end
  19. -- 等待玩家进入视野
  20. function Monster:state_wait_for_player()
  21. if self:get_distance(player) < self.range then
  22. self.direction = -self:get_direction_to(player)
  23. self:set_next_state(state_walk_2)
  24. end
  25. end
  26. -- 向反方向走 15
  27. function Monster:state_walk_2()
  28. local frame = self.state_frame_count;
  29. self:walk(self.direction)
  30. if frame > 15 then
  31. self:set_next_state(state_wait_for_player)
  32. end
  33. end

协程做法

  1. -- 每帧的逻辑
  2. function Monster:frame()
  3. -- 首先向右走 30
  4. for i = 1, 30 do
  5. self:walk(DIRECTION_RIGHT)
  6. self:wait()
  7. end
  8. while true do
  9. -- 等待玩家进入视野
  10. while self:get_distance(player) >= self.range do
  11. self:wait()
  12. end
  13. -- 向反方向走 15
  14. self.direction = -self:get_direction_to(player)
  15. for i = 1, 15 do
  16. self:walk(self.direction)
  17. self:wait()
  18. end
  19. end
  20. end
  21. -- 该帧结束
  22. function Monster:wait()
  23. coroutine.yield()
  24. end

额外说一句,从 wait 函数可以看出,Lua 的协程并不要求一定要从协程体函数中调用 yield,这是和 Python 的一个区别。

协同程序(coroutine,这里简称协程)是一种类似于线程(thread)的东西,它拥有自己独立的栈、局部变量和指令指针,可以跟其他协程共享全局变量和其他一些数据,并且具有一种挂起(yield)中断协程主函数运行,下一次激活恢复协程会在上一次中断的地方继续执行(resume)协程主函数的控制机制。

Lua 把关于协程的所有函数放在一个名为 “coroutine” 的 table 里,coroutine 里具有以下几个内置函数:

  1. -coroutine-yield [function: builtin#34]
  2. | -wrap [function: builtin#37]
  3. | -status [function: builtin#31]
  4. | -resume [function: builtin#35]
  5. | -running [function: builtin#32]
  6. | -create [function: builtin#33]

coroutine.create - 创建协程

函数 coroutine.create 用于创建一个新的协程,它只有一个以函数形式传入的参数,该函数是协程的主函数,它的代码是协程所需执行的内容

  1. co = coroutine.create(function()
  2. io.write("coroutine create!\n")
  3. end)
  4. print(co)

当创建完一个协程后,会返回一个类型为 thread 的对象,但并不会马上启动运行协程主函数,协程的初始状态是处于挂起状态

coroutine.status - 查看协程状态

协程有 4 种状态,分别是:挂起(suspended)、运行(running)、死亡(dead)和正常(normal),可以通过 coroutine.status 来输出查看协程当前的状态。

  1. print(coroutine.status(co))

coroutine.resume - 执行协程

函数 coroutine.resume 用于启动或再次启动一个协程的执行

  1. coroutine.resume(co)

协程被调用执行后,其状态会由挂起(suspended)改为运行(running)。不过当协程主函数全部运行完之后,它就变为死亡(dead)状态。

传递给 resume 的额外参数都被看作是协程主函数的参数

  1. co = coroutine.create(function(a, b, c)
  2. print("co", a, b, c)
  3. end)
  4. coroutine.resume(co, 1, 2, 3)

协程主函数执行完时,它的主函数所返回的值都将作为对应 resume 的返回值

  1. co = coroutine.create(function()
  2. return 3, 4
  3. end)
  4. print(coroutine.resume(co))

coroutine.yield - 中断协程运行

coroutine.yield 函数可以让一个运行中的协程中断挂起

  1. co = coroutine.create(function()
  2. for i = 1, 3 do
  3. print("before coroutine yield", i)
  4. coroutine.yield()
  5. print("after coroutine yield", i)
  6. end
  7. end)
  8. coroutine.resume(co)

coroutine.resume(co)
上面第一个 resume 唤醒执行协程主函数代码,直到第一个 yield。第二个 resume 激活被挂起的协程,并从上一次协程被中断 yield 的位置继续执行协程主函数代码,直到再次遇到 yield 或程序结束。

resume 执行完协程主函数或者中途被挂起(yield)时,会有返回值返回,第一个值是 true,表示执行没有错误。如果是被 yield 挂起暂停,yield 函数有参数传入的话,这些参数会接着第一个值后面一并返回

  1. co = coroutine.create(function(a, b, c)
  2. coroutine.yield(a, b, c)
  3. end)
  4. print(coroutine.resume(co, 1, 2, 3))

以 coroutine.wrap 的方式创建协程

跟 coroutine.create 一样,函数 coroutine.wrap 也是创建一个协程,但是它并不返回一个类型为 thread 的对象,而是返回一个函数。每当调用这个返回函数,都会执行协程主函数运行。所有传入这个函数的参数等同于传入 coroutine.resume 的参数。 coroutine.wrap 会返回所有应该由除第一个(错误代码的那个布尔量) 之外的由 coroutine.resume 返回的值。 和 coroutine.resume 不同之处在于, coroutine.wrap 不会返回错误代码,无法检测出运行时的错误,也无法检查 wrap 所创建的协程的状态

  1. function wrap(param)
  2. print("Before yield", param)
  3. obtain = coroutine.yield()
  4. print("After yield", obtain)
  5. return 3
  6. end
  7. resumer = coroutine.wrap(wrap)
  8. print(resumer(1))
  9. print(resumer(2))

coroutine.running - 返回正在运行中的协程

函数 coroutine.running 用于返回正在运行中的协程,如果没有协程运行,则返回 nil

  1. print(coroutine.running())
  2. co = coroutine.create(function()
  3. print(coroutine.running())
  4. print(coroutine.running() == co)
  5. end)
  6. coroutine.resume(co)
  7. print(coroutine.running())

resume-yield 交互

下面代码放在一个 lua 文件里运行,随便输入一些字符后按回车,则会返回输出刚才输入的内容

  1. function receive(prod)
  2. local status, value = coroutine.resume(prod)
  3. return value
  4. end
  5. function send(x)
  6. coroutine.yield(x)
  7. end
  8. function producer()
  9. return coroutine.create(function()
  10. while true do
  11. local x = io.read()
  12. send(x)
  13. end
  14. end)
  15. end
  16. function filter(prod)
  17. return coroutine.create(function()
  18. -- for line = 1, math.huge do
  19. for line = 1, 5 do
  20. local x = receive(prod)
  21. x = string.format("%5d Enter is %s", line, x)
  22. send(x)
  23. end
  24. end)
  25. end
  26. function consumer(prod)
  27. -- repeat
  28. -- local x = receive(prod)
  29. -- print(type(x))
  30. -- if x then
  31. -- io.write(x, "\n")
  32. -- end
  33. -- until x == nil
  34. while true do
  35. local obtain = receive(prod)
  36. if obtain then
  37. io.write(obtain, "\n\n")
  38. else
  39. break
  40. end
  41. end
  42. end
  43. p = producer()
  44. f = filter(p)
  45. consumer(f)

导航