• Feeds

  • Lua coroutine 不一样的多线程编程思路

    上周末开始看《Lua程序设计》第二版,目前体会到其中比较有趣的有两点,一是强大的table数据结构,另外就是coroutine。也许Lua中的coroutine是一种很好的设计模式,但我初步的体会还是没想到其他语言和场合能非常适合用到coroutine的场景。

    一、简介

    协同程序与线程差不多,也就是一条执行序列,拥有自己独立的栈,局部变量和指令指针,同时又与其它协同程序共享全局变量和其它大部分东西。线程与协同程序的主要区别在于,一个具有多线程的程序可以同时运行几个线程,而协同程序却需要彼此协作地运行。就是说,一个具有多个协同程序的程序在任何时刻只能运行一个协同程序,并且正在运行的协同程序只会在其显示地挂起时,它的执行才会暂停。

    如:

    co = coroutine.create(function ()
    for i=1,10 do
    print("co", i)
    coroutine.yield()
    end
    end)

    从主线程调用
    coroutine.resume(co)
    会依次打印1到10

    二、原理探析

    • coroutine创建的所谓的“线程”都不是真正的操作系统的线程,实际上是通过保存stack状态来模拟的。
    • 由于是假的线程,所以切换线程的开销极小,同时创建线程也是轻量级的,new_thread只是在内存新建了一个stack用于存放新coroutine的变量,也称作lua_State

    LUA_API lua_State *lua_newthread (lua_State *L)

    • 调用yield()当前线程交出控制权,同时还可以通过stack返回参数。调用resume的线程(可理解为主线程)获得返回的参数。
    • Lua yield()和Java中的Thread.yield()有点相似,但是区别更大。Java中的yield调用后只是将当前CPU切换到另外一个线程,CPU可能随时会继续回到线程执行。
    • 我更倾向于把Lua中的yield()和resume()和Java中的wait()和notify()来对比。它们表现的行为基本一致。
    • 关于stack实现也可参看Yufeng(Erlang高手)的分析文章 lua coroutine是如何实现的?

    三、Why coroutine?

    上面对coroutine有个基本的了解,因此大家都会象我一样去想,为什么要用coroutine?先研究下优点

    • 每个coroutine有自己私有的stack及局部变量。
    • 同一时间只有一个coroutine在执行,无需对全局变量加锁。
    • 顺序可控,完全由程序控制执行的顺序。而通常的多线程一旦启动,它的运行时序是没法预测的,因此通常会给测试所有的情况带来困难。所以能用coroutine解决的场合应当优先使用coroutine。

    再看缺点,研究coroutine缺点之前,我寻找了一下Lua中为什么实现coroutine的一些说明。在巴西人写的paper Coroutines in Lua(pdf)中解释了几个原因:

    • Lua是ANSI C实现的,ANSI C并不包含thread的实现,因此如果要在Lua增加thread的支持就要使用操作系统本地的实现,这样会造成通用的问题。同时也会使Lua变得臃肿。因此Lua选择了在ANSI C上实现的coroutine。
    • Lua主要设计目的之一是给C调用,如果Lua内部又有多线程实现的话会造成C调用状态的混乱,而只提供coroutine层面的挂起则可以保持状态的一致性。

    以上这些理由都是基于Lua特殊的原因而使用的,并不是很通用的原因。我们也了解到,coroutine实际上是一种古老的设计模式,它在60年代就已经定型,但是现代语言很少有重视这个特性,目前可以举例的有Windows的fibers, Python的generators

    四、Lua coroutine和Erlang

    上面优点有1条没展开,就是每个coroutine有自己私有的stack及内存变量空间。因此可以认为coroutine和Erlang中的process是非常相似的。但是coroutine只能同时只有一个在执行,如果能让他多个同时跑,我觉得就和Erlang非常相似了。

    《Lua程序设计》第二版30.2介绍的一种实现方法,让多个c threads启动,然后每个c thread启动一个coroutine(类似Erlang process),然后通过stack传递变量值(类似Erlang process message),这样就可以实现一个类似Erlang的process模型了。由于coroutine实际上可以用任何语言实现,那其他语言应该也可实现同样这种设计方法。

    五、Lua其他

    Lua目前主要用在游戏编程领域,通常的观点Lua是“胶水语言”。用来把各个模块化的功能粘合起来。就我目前阅读的一些代码来看,C和Lua通常是混合在一起的,并没有明确的边界。对于我一个外行的眼光看来我分不清哪些是在做C的事情,哪些是在调用Lua。特别是这个“胶水”如果放得太多,系统中各个模块的独立性将会受到影响。比如云风的这篇Lua 不是 C++也提到,“这属于过厚的粘合层,是绝对需要抛弃的”。

    另外Code@Pig一篇[网游设计] 一点感想也提到要简化调用,我总结它的观点主要两点:

    1. 不要存在冗余的关系,给一个部分负责管理就好。(由Lua/python来管理)
    2. 粘合层(Lua/python接口)不要过胖,我们可以通过引入一个“间接层”来把粘合层做“薄”

    虽然Lua的高效和精简的设计让人赞誉有加,但是它的性能排名并不高,和Python大致在同一个级别。另外“胶水语言”的定位也妨碍了它在更多领域的发展。

    如想及时阅读Tim Yang的文章,可通过页面右上方扫码订阅最新更新。

    « | »

    11 Comments  »

    1. Dimiter Stanev

      Hi Tim,

      With 32-bit lua-5.1.4 cygwin compiled I’m getting on my machine:
      elapsed time: 29.72

      but with 32-bit luajit (www.luajit.org) I’m getting:
      elapsed time: 7.06

      Cheers!

    2. Hey I am new here. I’m sorry for any reason if this this is the wrong place for thispost but I hope If some one here on timyang.net would be able to
      assist me to choose the better choice. The forums here are absolutely amazing and certainly plan on sticking around for as long as I am welcome.

    3. sw

      我觉得协程最大的应用是取代回调机制,当需要异步执行的时候,启动一个协程,然后当前协程挂起去处理其他事务。异步结果完成后再继续执行,看上去是阻塞但实际上不会影响其他应用,另外一个作用是实现复杂跌代器。

    4. db

      其实古老的、普及的VB就有coroutine的变种,而且应用的还很广泛。

    5. Zongjie Hu

      我觉得这个和python的twisted框架很像,应该算是一种异步框架。

    6. lzr

      我觉得你说的都是别人的东西,确没有自己的理解。

    Leave a Comment