免费注册 查看新帖 |

Chinaunix

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

[其他] 【(LUA相关)一个支持yield的server能避免掉轮询检查coroutine状态的命运吗?】 [复制链接]

论坛徽章:
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
1 [报告]
发表于 2013-02-24 14:28 |显示全部楼层
说实话没看懂,这样,我说一下coroutine和回调的联系吧。

假设我有一个回调接口在Lua上,这个接口的实现是很容易的,假设回调是这样的:

local server = require 'server' .new()

servern_connect(function(...)
   ...
end)

server:loop()

然后在连接的时候,调用on_connect注册的函数,当然还可以有很多的函数。在loop的时候等待客户端的连接,在accept的时候执行这个回调,这很明白了吧?

所谓的“coroutine”的优势,其实只有在多个回调的情况下才会出现。好,现在我们假设除了server以外,我们还有一个模块叫client,这个client模块负责产生某些连接:

local client = require 'client' .new()

client:connect(function(...)
  ....
en)
client:connect(function(...)
  ....
end)

client:loop()

我们创建了两个不同的连接,然后loop中对连接的结果进行回调,这个很简单吧?具体的实现大概是这样的:每次connect都会产生新的fd,然后会通过select/epoll完成loop的内容。注意!无论是select/epoll,都能在返回的时候实时反馈所有fd的状态。因此,在这两个函数返回的时候,你可以选择:

1. 调用有事件的函数的回调(比如cb("read", data...)或者cb("write", status...))
2. 当fd出错被关闭,将某些资源(主要是函数)标记为可以回收,当然回收前通知一下也可以

关键是!epoll不是通过轮询去获知所有的fd的状态的,而是会直接返回状态有改变的fd。对Lua来说,这也是一样的。

现在回到coroutine话题。这和回调是一一对应的。

1. 回调的注册,类似于心的coroutine的创建
2. 对回调的调用,类似于对coroutine的lua_resume
3. 对回调资源的清理,类似于对coroutine本身资源的清理

我们举个实际的例子。

假设我同时下载两个网站A和B的资源。现在用coroutine做这件事情。代码如下:

local res, err = client:download("A"
if err then ... end
print(res)

那么,操作是这样的:
1. 将上面的chunk做成一个function:lua_load(....)
2. 创建一个新的coroutine:lua_newthread(L);
3. 将chunk放到coroutine上面去:lua_xmove(或者直接在coroutine上lua_load)
4. 将coroutine和fd关联:registry.callbacks[fd] = co
5. loop的时候epoll/select
6. 当得知某个fd的状态改变时,查询到对应的co:co = registry.callbacks[fd]
7. 调用这个coroutine:lua_resume(co, ...)
8. 在清理fd的时候,清理那个注册表:registry.callbacks[fd] = nil

就是这样的。完全不需要轮询。

论坛徽章:
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
2 [报告]
发表于 2013-02-24 17:29 |显示全部楼层
回复 5# linux_c_py_php


    你的问题好奇怪,这样我先解答一下“coroutine何时结束”这样的问题,然后我来看看代码看看问题究竟出在哪儿= =

coroutine这种东西和thread这种东西不一样的地方是,这货的执行脉络不仅是可预知的,而且还是能被完全控制的。

什么意思呢?当你调用一个函数的时候,你会纠结“这个函数啥时候结束”么?不会吧?

当你为了某个目的需要连续调用一系列函数,你会纠结“这个函数何时结束”么?可能会吧,那怎么办呢?这时你需要通知coroutine,“我准备结束掉你”,然后等待coroutine自己结束自己,而且这个过程绝不会造成“coroutine就是不退出”这种尴尬的事情。

我来仔细的说明。

对于单次coroutine的“调度”,本质上就是将coroutine从“当前悬着的执行点”一直执行到“return或者yield”这么一个过程,当作一个函数去调用的过程。换句话说,lua_resume就是一个简单的函数而已。如果你有一个事件,这个事件完成了,我因为事件的完成需要回调,那么lua_resume好了,lua_resume以后coroutine必然有个状态——要么是继续别yield到另外一个状态了——这个时候你的yield函数自己应该已经记录了这个被yield的coroutine(如果没有,那么现在就是记录的时间了),要么你的coroutine应该已经执行完成了——这可以从lua_resume的返回值得到。如果执行完了,那么coroutine可以就可以扔掉(或者很简单地,不去记录),它自然会被垃圾回收掉。

如果是多次调用呢?这里就涉及到这个coroutine设计和整体的一个设计框架问题了。假设我有很多很多的阻塞函数,调用其他的任何一个都会导致coroutine被阻塞,coroutine的逻辑就在这不断地等待中继续。这种情况下,直接去调用coroutine.yield应该是违法的(你yield干嘛呢?),而是我去调用那些阻塞函数,阻塞函数记录你的事件存根(回调上下文)和coroutine的关系,然后yield,当事件产生时,根据存根找到coroutine(注意,这个关系本身会保持coroutine的生命),然后lua_resume,直到下一个阻塞函数记录了你的事件存根,或者因为执行完毕被咔嚓。上一段是从实现的角度分析,这一段是从coroutine的角度来观察。

那么,记住,coroutine本身绝对不是异步的,它的每次执行都是已知的,根本不存在异步的问题,而且coroutine本身不能用在多线程的环境中的,(Lua内部线程不安全,而如果人工设置lua_lock和lua_unlock会导致Lua锁住所有的执行,依然是一次执行一个过程),所以完全不存在通知的问题,这就是我觉得你的问题很奇怪的原因。

说白了,要知道coroutine何时结束,你必须知道coroutine如何开始,这就是我为什么在上一楼很详细地告诉你coroutine如何开始的原因,当coroutine通过某个函数调用(一定是lua_resume)开始以后,这个函数的返回必定是coroutine告一段落的时候,这个函数如果返回LUA_OK,那么coroutine必然就已经寿终正寝,否则coroutine内部做了yield,剩下的就完全是设计问题,跟coroutine本身没关系了。

千万弄清楚lua_resume和k系列函数的区别:
- lua_resume在任何情况下,都不可打断,不会跳转地开启一个coroutine,并一定会返回一个结果
- lua_(p)callk调用一个函数,这个函数可能会导致yield,如果yield,那么resume的时候,调用k作为代替
- lua_yieldk直接yield一个coroutine,在下次resume的时候,调用k作为执行的延续。

记住,开启一个coroutine的执行的时候,是不需要k的,而当你在coroutine内部害怕你会被下一层咔嚓掉的时候,就需要k来上保险了。


论坛徽章:
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
3 [报告]
发表于 2013-02-24 17:40 |显示全部楼层
本帖最后由 starwing83 于 2013-02-24 17:41 编辑

下面是一个GUI中采用coroutine作为事件驱动的实际例子,展示了采用coroutine作为事件驱动,对代码的极度简化。

只要写过GUI的人,我想肯定都写过如何拖动屏幕上的一个矩形这样的代码。这种代码需要处理各种状态:鼠标左键按下的时候做标记,鼠标移动的时候检查标志,等等。看看一个一coroutine驱动的脚本如何做这样的事情吧:
  1. -- 一个单个函数就能拖动矩形的程序

  2. local win = require 'grapapp' .new()
  3. local rect = require 'geometry' .rect(10, 10, 20, 20)

  4. -- 拖动逻辑
  5. win:mouse_event(function(evt)
  6.     if evt:wait() == 'down' and rect:contains(evt) then
  7.         local dx = rect.x - evt.x
  8.         local dy = rect.y - evt.y
  9.         while evt:wait() == 'move' do
  10.             rect.x = rect.x + evt.x
  11.             rect.y = rect.y + evt.y
  12.             win:redraw()
  13.         end
  14.     end
  15. end)

  16. -- 绘制逻辑
  17. win:redraw_event(function(evt)
  18.     evt.g:draw_rect(rect)
  19. end)
复制代码

论坛徽章:
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
4 [报告]
发表于 2013-02-24 18:32 |显示全部楼层
回复 14# linux_c_py_php


    先说几个通用策略吧。

1. 如果你想要实现的逻辑是“如果没有coroutine存在,那么main loop退出”,一个注册表就能完成。
2. lua_resume的调用成本,取决于你到底想做什么,事实上lua_resume的调用和lua_pcall一模一样——你管他yield不yield,因为阻塞函数本身就已经注册信息(或者没有注册信息,有什么关系呢?),resume以后直接返回事件循环即可。如果想知道事件循环什么时候退出,参见1
3. 可能你会想问,我想做些高级特性, 比如在coroutine上面pcall里面再resume(resume内部的代码如果error,会导致这个coroutine整体不可用),提供stack dump等等功能,我的看法,是包装一个,比如说,framework_resume的这样的函数做所有的繁杂事情,然后其他的接口直接调用这个函数。举个例子,其实pcall是很复杂的,需要推入traceback,还需要从object的userdata中解析出回调函数名等等,所以在我的gui框架里面,我有一个lga_self函数,这个函数负责处理好所有的pcall细节,然后我push所有参数,直接pcall即可。
4. 如果你真的觉得麻烦了,我可以告诉你:直接给Lua提供回调接口即可,coroutine机制完全可以直接建立在回调接口上!具体的实现你绝对可以独立思考出来,我就不细说了。

还有问题么?

论坛徽章:
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
5 [报告]
发表于 2013-02-24 18:47 |显示全部楼层
回复 17# linux_c_py_php


    你可以做一个表(lua_newtable),有一个coroutine,就放里面一个,每次事件之前检查这个表是不是空(检查lua_next调用返回的值),如果是空证明不再需要继续执行了。

论坛徽章:
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
6 [报告]
发表于 2013-02-24 18:59 |显示全部楼层
回复 20# linux_c_py_php


    ??????


你平时怎么处理回调的?回调如果执行过了,但是保留了一份状态,你需要知道这份状态需不需要继续保持么?你不需要对不对?(特别是针对有垃圾回收的语言)

你直接可以认为coroutine就是一系列可以连续回调的东西,回调的时候不需要知道这个回调的状态“有没有执行完”。

其实你还是没有一个完整设计coroutine的观念,而且就这么讨论我根本不可能知道你到底是哪儿理解错了。我建议这样:

你提一个你觉得需要轮询的场景,给出详细需求,我实现一份代码给你看。

你需要负责提供Lua层面的使用方法(Demo脚本),我负责提供能被require的库,当然所有网络请求我会做成在控制台输入文字控制的形式。(在控制台阻塞,你输入不同的命令模拟不同的情况,比如A请求已到,或者B请求出错等等)

论坛徽章:
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
7 [报告]
发表于 2013-02-24 19:23 |显示全部楼层
回复 25# linux_c_py_php


    你不需要知道啊= =为嘛要知道呢= =

有请求,就resume,没请求,就挂着,就这么简单啊= =需要知道到底结束了没么= =Lua是垃圾回收啊= =如果一个请求完成了或者甚至是他没有新的请求了,那么就会在之后某个时间被回收了嘛 = =

论坛徽章:
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
8 [报告]
发表于 2013-02-24 19:33 |显示全部楼层
回复 27# linux_c_py_php


    恩,你从头就错了。

你的服务器是一对多的,就是说,同时会处理很多个coroutine。

你需要在事件到达的时候,从一个映射表里面取得对应fd的那个coroutine,然后处理,然后pop掉。然后继续事件循环,再从有事件的那个fd取得另一个coroutine,然后处理,然后pop掉,不是一次处理完一个coroutine再下一个,这样就不是“协作式”了。

论坛徽章:
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
9 [报告]
发表于 2013-02-24 22:30 |显示全部楼层
回复 29# linux_c_py_php


    显然是你执迷不悟了。

一个coroutine对应的是一个fd,而不是一个请求,如果一个coroutine只对应一个请求,搞定了就销毁,那么你搞coroutine干嘛?coroutine的优点就是在处理多个请求的时候阻塞啊。单个请求难道不是处理完了就直接return么?

也就是说,一个fd发来一个请求,你resume这个coroutine,告诉他"new"了一个新的请求(对应GUI例子里面wait返回"down"),然后当有数据传递的时候,你resume这个coroutine高速他"read"新的数据(对应GUI例子里面wait返回"move"),coroutine是不是处理这个请求,是不是读完所有数据,coroutine说了算,如果它dead了,就代表它不打算处理完所有数据了,直接喀嚓掉这个fd。

如果当前请求完毕,再有新的请求到来,继续resume那个coroutine,告诉他"new"了一个新的请求,其余的就完全一样了。

coroutine和fd相关,不和请求相关。

如果要和请求相关也容易,预先保存一份chunk(Lua代码),当"new"的时候,判断coroutine是不是有yield的内容, 不管是什么情况,都直接error出来到pcall,然后继续push那个chunk,然后resume,这样一个coroutine就可以被反复利用,从逻辑上就是一个请求一个coroutine,请求完毕coroutine被废,但物理上coroutine是被反复利用的。

再强调一点,来一个事件(新请求或者读请求都是事件,甚至出错都是时间),告诉coroutine事件的类型,然后执行一段代码,然后就不管了。是resume控制coroutine执行,而不是反过来coroutine告诉自己执行完了没——resume返回就意味着coroutine一定执行完了。

论坛徽章:
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
10 [报告]
发表于 2013-02-24 22:40 |显示全部楼层
windoze是对的,事实上coroutine的优势就是可以跟线程完全没关系,所有过程都是同步/线性的,只有coroutine本身的执行是被交织在一起的,但是它们本身的执行过程依然是同步的,没有任何的并发。

另一个优势是,要并发也是非常容易的:建立多个State即可,你必须保证不要使用全局变量——当然,Lua5.2也已经没有全局变量了= =
您需要登录后才可以回帖 登录 | 注册

本版积分规则 发表回复

  

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

清除 Cookies - ChinaUnix - Archiver - WAP - TOP