免费注册 查看新帖 |

Chinaunix

  平台 论坛 博客 文库
12
最近访问板块 发新帖
楼主: linux_c_py_php
打印 上一主题 下一主题

[其他] 【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
11 [报告]
发表于 2013-01-16 00:31 |只看该作者
本帖最后由 linux_c_py_php 于 2013-01-16 00:32 编辑

但是我在lua里实现相同的东西, 再次resume的返回点却是callback函数内部, 而不是像c_callback那样直接返回到wrap调用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. function callback(f)
  11.     n = f()
  12.     print(n)
  13. end

  14. co = coroutine.create(function()
  15.   print('coroutine yielding')   
  16.   callback(function()         
  17.     coroutine.yield()
  18.     return 5
  19.   end)
  20.   print('coroutine resumed')   
  21. end)                             
  22.                                  
  23. coroutine.resume(co)
  24. coroutine.resume(co)
  25.                                  
  26. print('the end')  
复制代码
linux_c_py_php 发表于 2013-01-15 23:42
@starwing83

哥, 再指导指导吧, 我快找到感觉了.

论坛徽章:
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. }
复制代码

论坛徽章:
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
14 [报告]
发表于 2013-01-16 11:49 |只看该作者
非常感谢starwing83, 很受用, 有问题再找你哈.

论坛徽章:
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堆栈了吧.

论坛徽章:
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
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
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
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
20 [报告]
发表于 2013-01-21 20:39 |只看该作者
回复 19# linux_c_py_php


    sigh……方兆国要是也这样多好= =
您需要登录后才可以回帖 登录 | 注册

本版积分规则 发表回复

  

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

清除 Cookies - ChinaUnix - Archiver - WAP - TOP