免费注册 查看新帖 |

Chinaunix

  平台 论坛 博客 文库
12下一页
最近访问板块 发新帖
查看: 12218 | 回复: 19
打印 上一主题 下一主题

[其他] 【lua5.2相关】 今天做了一番试验, 发现coroutine有此坑, 真神秘。 [复制链接]

论坛徽章:
4
水瓶座
日期:2013-09-06 12:27:30摩羯座
日期:2013-09-28 14:07:46处女座
日期:2013-10-24 14:25:01酉鸡
日期:2014-04-07 11:54:15
跳转到指定楼层
1 [收藏(0)] [报告]
发表于 2013-01-15 15:11 |只看该作者 |正序浏览
环境是lua5.2, 主要是把5.1->5.2的api change list过了一遍, 主要想验证coroutine的工作过程, 然后发现了两个坑, 大侠看看我理解的对不对, 多谢.

1, 写C模块命名为"mod", 写C宿主调用"mod", 其中"mod"模块只包含一个函数, 用于测试coroutine.
  1. [root@vps616 study]# cat src/mod.c
  2. #include "lua.h"
  3. #include "lualib.h"
  4. #include "lauxlib.h"

  5. static int run_twice(lua_State *L) {
  6.     int ctx;

  7.     if (lua_getctx(L, &ctx) == LUA_OK) {
  8.         lua_pushstring(L, "hello");
  9.         return lua_yieldk(L, 1, 2, run_twice);
  10.     }
  11.     lua_pushstring(L, " world");
  12.     return 1;
  13. }

  14. static const luaL_Reg reg_api[] = {
  15.     {"run_twice", run_twice},
  16.     {NULL, NULL}
  17. };

  18. int luaopen_mod(lua_State *L) {
  19.     lua_newtable(L);
  20.     luaL_setfuncs(L, reg_api, 0);
  21.     return 1;
  22. }
复制代码
  1. [root@vps616 study]# cat src/main.c
  2. #include "lua.h"
  3. #include "lualib.h"
  4. #include "lauxlib.h"

  5. int main(int argc, char* const argv[]) {
  6.     lua_State *L = luaL_newstate();
  7.     lua_State *LL = lua_newthread(L);
  8.    
  9.     lua_pushcfunction(L, luaopen_base);
  10.     if (lua_pcall(L, 0, 0, 0) != LUA_OK) {
  11.         return 1;
  12.     }
  13.     lua_pushcfunction(L, luaopen_package);
  14.     if (lua_pcall(L, 0, 0, 0) != LUA_OK) {
  15.         return 2;
  16.     }
  17.     lua_getglobal(L, "require");
  18.     lua_pushstring(L, "mod");
  19.     if (lua_pcall(L, 1, 1, 0) != LUA_OK) {
  20.         return 3;
  21.     }
  22.     lua_getfield(L, -1, "run_twice");
  23.     lua_pushstring(LL, "1234");
  24.     printf("top=%d\n", lua_gettop(LL));
  25.     lua_xmove(L, LL, 1);
  26.     printf("top=%d\n", lua_gettop(LL));
  27.     if (lua_resume(LL, NULL, 0) == LUA_YIELD) {
  28.         printf("yield=%s\n", lua_tostring(LL, -1));
  29.     }
  30.     printf("top=%d\n", lua_gettop(LL));
  31.     if (lua_resume(LL, NULL, 0) == LUA_OK) {
  32.         printf("return=%s\n", lua_tostring(LL, -1));
  33.     }
  34.     printf("top=%d %s\n", lua_gettop(LL), lua_tostring(LL, 1));
  35.     return 0;
  36. }
复制代码
这个例子主要目的是观察run_twice在协程中执行的栈变化, 因为看lua.org/manual实在是描述的不太清晰.
  1. [root@vps616 study]# bin/main
  2. top=1
  3. top=2
  4. yield=hello
  5. top=1
  6. return= world
  7. top=2 1234
复制代码
执行结果如下, 可见, yield=hello后top=1, 是yield的返回值, 而return= world之后top=2, 栈底是1234, 栈顶是 world.
我得到结论就是: 在协程未return消亡前, 协程的栈只会容纳resume与yield彼此交换的参数, 直到return才会重新将原本thread栈上的值暴露出来.
这一点和直接调用一个普通函数一样, 在函数内部(相当于thread未return)来看栈只容纳了参数, 其他参数与函数之前的内容暂时屏蔽掉了.


上面这个感悟是否和谐, 求指正.



再就是之前在5.1里被lua_yield坑过, 那个版本yield只能抛回给lua函数而不能抛回给c函数, 5.2有了k系列就不一样了, 但我看5.2手册里lua_yield的说明太朦胧:
int lua_yield (lua_State *L, int nresults);

This function is equivalent to lua_yieldk, but it has no continuation (see §4.7). Therefore, when the thread resumes, it returns to the function that called the function calling lua_yield.


我以为这句话的描述是lua_yield相当于lua_yieldk的function传入当前的function, 但实际上当再次resume的时候并没有重新调用发起lua_yield的那个函数, 而是当再次resume直接返回true, 栈也是空的, 我就是很郁闷了.

根据这个结论, 我认为lua_yield是在lua_pcallk/lua_callk的callee函数中调用的, 这样再次resume执行的是lua_pcallk/lua_callk指定的function。

这个结论是否和谐?

论坛徽章:
5
狮子座
日期:2013-08-20 10:12:24午马
日期:2013-11-23 18:04:102015年辞旧岁徽章
日期:2015-03-03 16:54:152015亚冠之德黑兰石油
日期:2015-06-29 18:11:1115-16赛季CBA联赛之新疆
日期:2024-02-21 10:00:53
20 [报告]
发表于 2013-01-21 20:39 |只看该作者
回复 19# linux_c_py_php


    sigh……方兆国要是也这样多好= =

论坛徽章:
4
水瓶座
日期:2013-09-06 12:27:30摩羯座
日期:2013-09-28 14:07:46处女座
日期:2013-10-24 14:25:01酉鸡
日期:2014-04-07 11:54:15
19 [报告]
发表于 2013-01-21 20:31 |只看该作者

very good, thanks very very much!

starwing83 发表于 2013-01-21 19:16
回复 17# linux_c_py_php

论坛徽章:
5
狮子座
日期:2013-08-20 10:12:24午马
日期:2013-11-23 18:04:102015年辞旧岁徽章
日期:2015-03-03 16:54:152015亚冠之德黑兰石油
日期:2015-06-29 18:11:1115-16赛季CBA联赛之新疆
日期:2024-02-21 10:00:53
18 [报告]
发表于 2013-01-21 19:16 |只看该作者
回复 17# linux_c_py_php


    对对,就是这个原理。

在Lua的堆栈链上,可以不止一个C函数的,可能这样:C->Lua->Lua->C->Lua->C这么交替的,如果在最顶上yield了,那么所有Lua函数的栈都可以得到保持,所有C函数的Lua栈也没问题。不过C自己的栈就完蛋了。这个时候就去调用一个cont函数,传递给它保存完整的Lua栈,然后爱咋地它就咋地吧~

说白了就是Lua提供了一个回调函数,告诉你:“原函数被咔嚓了,不过我们又回到原函数被咔嚓的下一行了,你看着办吧”,回调执行完以后就继续当作没事儿人一样继续往下走了。

论坛徽章:
4
水瓶座
日期:2013-09-06 12:27:30摩羯座
日期:2013-09-28 14:07:46处女座
日期:2013-10-24 14:25:01酉鸡
日期:2014-04-07 11:54:15
17 [报告]
发表于 2013-01-21 19:01 |只看该作者
晚上回家会自己试验一下, 大哥有空也给指教指教.

我的感受是lua是解释器执行, 所以它的堆栈是可以维持的, 并在再次resume时, 避开c.callback的堆栈破坏不说, 应当可以正常从c.callback(c_cont)返回.
但如果这个wrapper是c的,那么resume的这个wrapper的C堆栈也会被破坏,没法继续执行。

linux_c_py_php 发表于 2013-01-21 18:58
@starwing83

追加个问题, 对于blog里的例子, 如果coroutine.create的这个function我不用lua写, 而是在c宿 ...

论坛徽章:
5
狮子座
日期:2013-08-20 10:12:24午马
日期:2013-11-23 18:04:102015年辞旧岁徽章
日期:2015-03-03 16:54:152015亚冠之德黑兰石油
日期:2015-06-29 18:11:1115-16赛季CBA联赛之新疆
日期:2024-02-21 10:00:53
16 [报告]
发表于 2013-01-21 19:01 |只看该作者
回复 15# linux_c_py_php


    对。

其实c_cont执行时刻,Lua所有的状态,就好像lua_pcall(调用yield的那个,或者你就直接yield了)之后成功的状态。栈上是pcall的返回值,啥事情都没有。除了你现在在c_cont里而不是在c_callback里以外= =

论坛徽章:
4
水瓶座
日期:2013-09-06 12:27:30摩羯座
日期:2013-09-28 14:07:46处女座
日期:2013-10-24 14:25:01酉鸡
日期:2014-04-07 11:54:15
15 [报告]
发表于 2013-01-21 18:58 |只看该作者
@starwing83

追加个问题, 对于blog里的例子, 如果coroutine.create的这个function我不用lua写, 而是在c宿主程序里传入一个c的function(先调用printf,再调用lua里的c.function,再调用c的printf)并在c里resume, 那么当再次resume时, 执行c_cont后return到的应该直接是resume点, 而不会是c.callback的调用点吧, 第一次yield应该已经破坏掉了除了main thread以外的所有C堆栈了吧.

论坛徽章:
4
水瓶座
日期:2013-09-06 12:27:30摩羯座
日期:2013-09-28 14:07:46处女座
日期:2013-10-24 14:25:01酉鸡
日期:2014-04-07 11:54:15
14 [报告]
发表于 2013-01-16 11:49 |只看该作者
非常感谢starwing83, 很受用, 有问题再找你哈.

论坛徽章:
5
狮子座
日期:2013-08-20 10:12:24午马
日期:2013-11-23 18:04:102015年辞旧岁徽章
日期:2015-03-03 16:54:152015亚冠之德黑兰石油
日期:2015-06-29 18:11:1115-16赛季CBA联赛之新疆
日期:2024-02-21 10:00:53
13 [报告]
发表于 2013-01-16 10:19 |只看该作者
回复 12# linux_c_py_php


    这样,我详细说一遍吧。

无论是C还是Lua,要执行就需要两样东西,一个叫“执行上下文”,一个叫做“瞬间状态”。

对C而言,上下文就是所有的堆栈,而瞬间状态就是在执行的某一刻所有的寄存器,包括PC指针(执行到哪个指针了)

对Lua而言,上下文同样指所有的堆栈,而因为对Lua而言堆栈就是寄存器,所以剩下的还有一个就是PC指针(这个不在寄存器中,是独立存在于Lua状态里的)。

好,说到这里,我们可以确定,给你一段堆栈,给你一个瞬间状态,你就能继续执行下去(怎么执行呢?从当前的瞬间开始,继续执行下一个指令)。

如果这里没有Lua,只有C呢?比如我的调用流程是这样的:

  1. main函数 -> funca -> funcb -> funcc -> funcd *
  2.                                  ^在这里开始记录堆栈      ^ 在这里记录瞬间状态
复制代码
这就是所谓的纤程,或者Unix的ucontext了。

从某个函数开始记录堆栈,一直记录下去,直到到某个点停止记录,这个时候我们获取了一段堆栈,然后也获取了停止记录的瞬间状态了。然后我们回到了funcb位置,再调用函数什么的,然后从funcb开始恢复记录的堆栈和瞬间状态,那么我们就恢复到那个停止记录的状态了。

这里需要注意两个关键点:
1. 如果堆栈是有结构的(就是说,每节堆栈都好认),那么从funcb返回,然后在main开始恢复也是可以的,这时候堆栈信息是这样的:

  1. main -> funcb -> funcc -> funcd *
复制代码
你可以认为一个函数的所有堆栈是个积木,我们把从某个地点开始的一段积木的颜色记录下来,直到最高的地方记录一个顶,然后在其他的调用函数的地方,我们不是推入一节堆栈用普通的函数,而是推入一堆我们之前记录的那一段积木。因此就不是那么容易能返回了。要把这些积木都return掉了才能返回到推入的那个点,除非你再记录一次瞬间状态然后把这些堆栈都扔掉,回到刚才那个点(就是yield啦)。

但是这样会有一个效率问题。因为resume的时候要把很大一截堆栈给拷贝到当前的堆栈上。怎么办呢?最简单的方式就是多堆栈。在一个堆栈上我们执行到某个点,然后中断,到另一个堆栈上开始执行。对第一个堆栈来说,这个行为和普通函数调用无异,而对第二个堆栈来说,这和普通函数调用也很像,只是是从某个之前被切出的调用点切入而已,不是构造新的堆栈块,而是恢复之前的瞬间状态。然后在某个时间,记录所有的瞬间状态到堆栈,然后返回到第一个堆栈。

而这,就基本上是正常的单语言coroutine的执行流程了。需要仔细思考,然后进入下一章节:双语言coroutine。

……OK,上面的想清楚了吧?现在进入双语言coroutine环节。在上面,我们假设了语言要有这样几个功能:
1. 构建新的堆栈
2. 记录瞬间状态
3. 切换堆栈
4. 恢复瞬间状态

其中1就是coroutine.create,而2+3+4就是resume或者yield。从当前的堆栈切出去,然后等着切回来,就是resume,从当前的堆栈切出去然后不准备返回了,就是yield。说白了,resume就是一个函数调用,以resume开始,然后以对方的yield结束。而yield则相当于跨堆栈的return了。

那么如果涉及到C层面呢?resume就是setjmp,然后记录当前Lua栈的状态。yield就是记录当前的状态(其实不用记录,因为Lua的状态就在coroutine里面,一直都存在),然后longjmp。因为resume和yield都是2+3+4,因此如果它们用相同的过程,就是对称式的coroutine,否则如同Lua这样,就是不对称的coroutine。

好了,可以想见一点,Lua的状态是不会有任何损失的。因为coroutine可以完全记录所有的运行过程。但是C怎么办呢?上面的四个能力,C一个都没有。

我们的办法是这样的。当longjmp的时候,longjmp所在的那一整段的C栈就都丢失了。在下次resume的时候,实际上是重新执行了整个Lua内部的状态,Lua知道自己要做什么,所以即使丢了C栈也无所谓——从逻辑上我们知道这时候应该做什么(看源代码,precall+evaluate+postcall什么的),但是C API部分的丢了就完全丢了,在回来也不知道在哪儿了。这个时候怎么办呢?

Lua5.1里面,这就没办法了。如果发现有客户端的C栈会丢(比如,当前栈上有C函数还在执行),那么就报错,说不能跨越C边界)。在5.2里面,我们每次在C中执行Lua的过程的时候,或者从C转出去的时候,(前者是callk,后者是yieldk)都要指定一个continuation函数。这个函数就是用于在转出去以后要转回来的时候被调用,代替被扔掉的C栈的。这时就不会产生这个错误。但是,如果C栈没有提供那个continuation(即传递NULL代替continuation),那么依然会产生这个错误。

我们举个例子。就用我博客里面那个例子。首先从Lua开始执行。当调用C的callback函数时,传入了一个新的Lua函数。在c_callback里,我们调用pcallk,这时流程被转入了Lua,我们同时记录了一个continuation,否则在Lua里面yield直接就报错了(和5.1兼容)。现在在Lua层面我们yield了,这时会resume到Lua里面。我们的c_callback的栈帧,包括Lua自己的一堆栈帧都被扔掉了,回到了上个setjmp位置(即Lua里面的resume调用),这时Lua继续执行。

Lua从resume返回,然后执行下一行语句,这一行又是resume。这时我们回到那个coroutine,开始执行被中断的Lua函数,Lua从yield开始执行,然后Lua函数返回,理论上这里应该返回到c_callback的pcall的下一行了,但是c_callback的栈帧丢了,我们无法回到这里了。所以我们调用c_callback的continuation,即c_cont函数,对c_cont函数来说,我们现在就好像在c_callback里面刚刚从pcallk返回的那个状态。完全没有任何变化。我们从c_cont返回,Lua就认为c.callback已经执行完毕。然后返回,这时返回了setjmp的resume,resume返回。Lua继续执行下一行语句。

整个过程就是这样,仔细体会就能明白。如果还有不懂的欢迎留言~








论坛徽章:
4
水瓶座
日期:2013-09-06 12:27:30摩羯座
日期:2013-09-28 14:07:46处女座
日期:2013-10-24 14:25:01酉鸡
日期:2014-04-07 11:54:15
12 [报告]
发表于 2013-01-16 00:39 |只看该作者
本帖最后由 linux_c_py_php 于 2013-01-16 00:58 编辑

与之对应的C实现只能是这样, 根本不会从pcallk返回, 而且是直接从c_cont中return到c.callback被调用位置, 为何有差别? 无法实现成一致的么? 什么道理?
  1. [root@vps616 study]# ./main.lua   
  2. coroutine yielding
  3. 5
  4. coroutine resumed
  5. the end
  6. [root@vps616 study]# cat main.lua
  7. #!/usr/bin/env lua

  8. c = require('c')                 
  9.   
  10. co = coroutine.create(function()
  11.   print('coroutine yielding')   
  12.   n = c.callback(function()         
  13.     coroutine.yield()
  14.   end)
  15.   print(n)
  16.   print('coroutine resumed')   
  17. end)                             
  18.                                  
  19. coroutine.resume(co)
  20. coroutine.resume(co)
  21.                                  
  22. print('the end')  
  23. [root@vps616 study]# cat src/mod.c
  24. #include<stdio.h>                                                     
  25. #include<stdlib.h>                                                   
  26. #include<lua.h>                                                      
  27. #include<lualib.h>                                                   
  28. #include<lauxlib.h>                                                   
  29.                                                                        
  30. static int c_cont(lua_State *L) {                                    
  31.     lua_pushinteger(L, 5);
  32.     return 1;  //这里返回到了c_callback被调用的位置, 也就是lua里的调用点                                                      
  33. }                                                                     
  34.                                                                        
  35. static int c_callback(lua_State *L){                                 
  36.   int ret = lua_pcallk(L, 0, 0, 0, 0, c_cont);                        
  37.   if(ret) {                                                           
  38.     fprintf(stderr, "Error: %s\n", lua_tostring(L, -1));              
  39.     lua_pop(L, 1);                                                   
  40.     exit(1);                                                         
  41.   }                                                                  
  42.   return 0;
  43. }                                                                     
  44.                                                                        
  45. static const luaL_Reg c[] = {                                         
  46.   {"callback", c_callback},                                          
  47.   {NULL, NULL}                                                        
  48. };                                                                    
  49.                                                                        
  50. int luaopen_c (lua_State *L) {                             
  51.   /* 使用新的 luaL_newlib 函数 */                                    
  52.   luaL_newlib(L, c);                                                  
  53.   return 1;                                                           
  54. }
复制代码
  

北京盛拓优讯信息技术有限公司. 版权所有 京ICP备16024965号-6 北京市公安局海淀分局网监中心备案编号:11010802020122 niuxiaotong@pcpop.com 17352615567
未成年举报专区
中国互联网协会会员  联系我们:huangweiwei@itpub.net
感谢所有关心和支持过ChinaUnix的朋友们 转载本站内容请注明原作者名及出处

清除 Cookies - ChinaUnix - Archiver - WAP - TOP