Lua 协同程序(coroutine)

什么是协同(coroutine)?

Lua 协同程序(coroutine)与线程比较类似:拥有独立的堆栈,独立的局部变量,独立的指令指针,同时又与其它协同程序共享全局变量和其它大部分东西。

协同是非常强大的功能,但是用起来也很复杂。

线程和协同程序区别

线程与协同程序的主要区别在于,一个具有多个线程的程序可以同时运行几个线程,而协同程序却需要彼此协作的运行。

在任一指定时刻只有一个协同程序在运行,并且这个正在运行的协同程序只有在明确的被要求挂起的时候才会被挂起。

协同程序有点类似同步的多线程,在等待同一个线程锁的几个线程有点类似协同。

基本语法

方法 描述
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 的线程号

以下实例演示了以上各个方法的用法:

coroutine_test.lua 文件

  1. -- coroutine_test.lua 文件
  2. co = coroutine.create(
  3. function(i)
  4. print(i);
  5. end
  6. )
  7. coroutine.resume(co, 1) -- 1
  8. print(coroutine.status(co)) -- dead
  9. print("----------")
  10. co = coroutine.wrap(
  11. function(i)
  12. print(i);
  13. end
  14. )
  15. co(1)
  16. print("----------")
  17. co2 = coroutine.create(
  18. function()
  19. for i=1,10 do
  20. print(i)
  21. if i == 3 then
  22. print(coroutine.status(co2)) --running
  23. print(coroutine.running()) --thread:XXXXXX
  24. end
  25. coroutine.yield()
  26. end
  27. end
  28. )
  29. coroutine.resume(co2) --1
  30. coroutine.resume(co2) --2
  31. coroutine.resume(co2) --3
  32. print(coroutine.status(co2)) -- suspended
  33. print(coroutine.running())
  34. print("----------")
  35. `

以上实例执行输出结果为:

  1. 1
  2. dead
  3. ----------
  4. 1
  5. ----------
  6. 1
  7. 2
  8. 3
  9. running
  10. thread: 0x7fb801c05868 false
  11. suspended
  12. thread: 0x7fb801c04c88 true
  13. ----------

coroutine.running就可以看出来,coroutine在底层实现就是一个线程。

当create一个coroutine的时候就是在新线程中注册了一个事件。

当使用resume触发事件的时候,create的coroutine函数就被执行了,当遇到yield的时候就代表挂起当前线程,等候再次resume触发事件。

接下来我们分析一个更详细的实例:

实例

  1. function foo (a)
  2. print("foo 函数输出", a)
  3. return coroutine.yield(2 * a) -- 返回 2*a 的值
  4. end
  5. co = coroutine.create(function (a , b)
  6. print("第一次协同程序执行输出", a, b) -- co-body 1 10
  7. local r = foo(a + 1)
  8. print("第二次协同程序执行输出", r)
  9. local r, s = coroutine.yield(a + b, a - b) -- ab的值为第一次调用协同程序时传入
  10. print("第三次协同程序执行输出", r, s)
  11. return b, "结束协同程序" -- b的值为第二次调用协同程序时传入
  12. end)
  13. print("main", coroutine.resume(co, 1, 10)) -- true, 4
  14. print("--分割线----")
  15. print("main", coroutine.resume(co, "r")) -- true 11 -9
  16. print("---分割线---")
  17. print("main", coroutine.resume(co, "x", "y")) -- true 10 end
  18. print("---分割线---")
  19. print("main", coroutine.resume(co, "x", "y")) -- cannot resume dead coroutine
  20. print("---分割线---")

以上实例执行输出结果为:

  1. 第一次协同程序执行输出 1 10
  2. foo 函数输出 2
  3. main true 4
  4. --分割线----
  5. 第二次协同程序执行输出 r
  6. main true 11 -9
  7. ---分割线---
  8. 第三次协同程序执行输出 x y
  9. main true 10 结束协同程序
  10. ---分割线---
  11. main false cannot resume dead coroutine
  12. ---分割线---

以上实例接下如下:

  • 调用resume,将协同程序唤醒,resume操作成功返回true,否则返回false;
  • 协同程序运行;
  • 运行到yield语句;
  • yield挂起协同程序,第一次resume返回;(注意:此处yield返回,参数是resume的参数)
  • 第二次resume,再次唤醒协同程序;(注意:此处resume的参数中,除了第一个参数,剩下的参数将作为yield的参数)
  • yield返回;
  • 协同程序继续运行;
  • 如果使用的协同程序继续运行完成后继续调用 resume方法则输出:cannot resume dead coroutine

resume和yield的配合强大之处在于,resume处于主程中,它将外部状态(数据)传入到协同程序内部;而yield则将内部的状态(数据)返回到主程中。

生产者-消费者问题

现在我就使用Lua的协同程序来完成生产者-消费者这一经典问题。

实例

  1. local newProductor
  2. function productor()
  3. local i = 0
  4. while true do
  5. i = i + 1
  6. send(i) -- 将生产的物品发送给消费者
  7. end
  8. end
  9. function consumer()
  10. while true do
  11. local i = receive() -- 从生产者那里得到物品
  12. print(i)
  13. end
  14. end
  15. function receive()
  16. local status, value = coroutine.resume(newProductor)
  17. return value
  18. end
  19. function send(x)
  20. coroutine.yield(x) -- x表示需要发送的值,值返回以后,就挂起该协同程序
  21. end
  22. -- 启动程序
  23. newProductor = coroutine.create(productor)
  24. consumer()

以上实例执行输出结果为:

  1. 1
  2. 2
  3. 3
  4. 4
  5. 5
  6. 6
  7. 7
  8. 8
  9. 9
  10. 10
  11. 11
  12. 12
  13. 13
  14. ……

这一章的例子较难理解,如果把yield()和resume()两个函数的行为描述清楚了,就好理解多了。

例子再简化一下:

  1. co = coroutine.create(function (a)
  2. local r = coroutine.yield(a+1) -- yield()返回a+1给调用它的resume()函数,即2
  3. print("r=" ..r) -- r的值是第2resume()传进来的,100
  4. end)
  5. status, r = coroutine.resume(co, 1) -- resume()返回两个值,一个是自身的状态true,一个是yield的返回值2
  6. coroutine.resume(co, 100) --resume()返回true

coroutine.creat方法和coroutine.wrap需要特别注意的是这个返回值的类型,功能上有些类似,但并不完全一样。

coroutine.creat返回的是一个协同程序,类型为thread,需要使用coroutine.resume进行调用;而coroutine.wrap返回的是一个普通的方法(函数),类型为function,和普通function有同样的使用方法,并且不能使用coroutine.resume进行调用。

以下代码进行测试:

  1. co_creat = coroutine.create(
  2. function()
  3. print("co_creat类型是"..type(co_creat))
  4. end
  5. )
  6. co_wrap = coroutine.wrap(
  7. function()
  8. print("co_wrap类型是"..type(co_wrap))
  9. end
  10. )
  11. coroutine.resume(co_creat)
  12. co_wrap()

输出:

  1. co_creat类型是thread
  2. co_wrap类型是function

coroutine.resume方法需要特别注意的一点是,这个方法只要调用就会返回一个boolean值。

coroutine.resume方法如果调用成功,那么返回true,如果有yield方法,同时返回yield括号里的参数;如果失败,那么返回false,并且带上一句”cannot resume dead coroutine”

以下代码进行测试:

  1. co_yieldtest = coroutine.create(
  2. function()
  3. coroutine.yield()
  4. coroutine.yield(1)
  5. return 2
  6. end
  7. )
  8. for i = 1,4 do
  9. print("第"..i.."次调用协程:", coroutine.resume(co_yieldtest))
  10. end

输出:

  1. 1次调用协程: true
  2. 2次调用协程: true 1
  3. 3次调用协程: true 2
  4. 4次调用协程: false cannot resume dead coroutine

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”

以下代码进行测试:

  1. function yieldReturn(arg) return arg end
  2. co_yieldtest = coroutine.create(
  3. function()
  4. print("启动协程状态"..coroutine.status(co_yieldtest))
  5. print("--")
  6. coroutine.yield()
  7. coroutine.yield(1)
  8. coroutine.yield(print("第3次调用"))
  9. coroutine.yield(yieldReturn("第4次调用"))
  10. return 2
  11. end
  12. )
  13. print("启动前协程状态"..coroutine.status(co_yieldtest))
  14. print("--")
  15. for i = 1,6 do
  16. print("第"..i.."次调用协程:", coroutine.resume(co_yieldtest))
  17. print("当前协程状态"..coroutine.status(co_yieldtest))
  18. print("--")
  19. end

输出:

  1. 启动前协程状态suspended
  2. --
  3. 启动协程状态running
  4. --
  5. 1次调用协程: true
  6. 当前协程状态suspended
  7. --
  8. 2次调用协程: true 1
  9. 当前协程状态suspended
  10. --
  11. 3次调用
  12. 3次调用协程: true
  13. 当前协程状态suspended
  14. --
  15. 4次调用协程: true 4次调用
  16. 当前协程状态suspended
  17. --
  18. 5次调用协程: true 2
  19. 当前协程状态dead
  20. --
  21. 6次调用协程: false cannot resume dead coroutine
  22. 当前协程状态dead
  23. --

挂起协程: yield 除了挂起协程外,还可以同时返回数据给 resume ,并且还可以同时定义下一次唤醒时需要传递的参数。

  1. print();
  2. cor = coroutine.create(function(a)
  3. print("参数 a值为:", a);
  4. local b, c = coroutine.yield(a + 1); --这里表示挂起协程,并且将a+1的值进行返回,并且指定下一次唤醒需要 b,c 两个参数。
  5. print("参数 b,c值分别为:", b, c);
  6. return b * c; --协程结束,并且返回 b*c 的值。
  7. end);
  8. print("第一次调用:", coroutine.resume(cor, 1));
  9. print("第二次调用:", coroutine.resume(cor, 2, 2));
  10. print("第三次调用:", coroutine.resume(cor));

执行结果(结果中 true 表示本次调用成功):

  1. 参数 a值为: 1
  2. 第一次调用: true 2
  3. 参数 b,c值分别为: 2 2
  4. 第二次调用: true 4
  5. 第三次调用: false cannot resume dead coroutine

正文中的例子分析:

第一次调用(第一次调用的时候,协同程序是一个挂起的状态),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 的参数 作为主函数的参数。