zoukankan      html  css  js  c++  java
  • Lua的多任务机制——协程(coroutine)

        并发是现实世界的本质特征,而聪明的计算机科学家用来模拟并发的技术手段便是多任务机制。大致上有这么两种多任务技术,一种是抢占式多任务(preemptive multitasking),它让操作系统来决定何时运行哪个任务。第二种就是协作式多任务(cooperative multitasking),它把决定权交给任务,让它们在自己觉得合适的时候自愿放弃运行。这两种多任务方式各有优缺点,前者固有的同步问题使得程序常常有不可预知的行为,而后者则要求任务具备相当的自律精神。

        协程(coroutine)技术是一种程序控制机制,早在上世纪60年代就已提出,用它能够非常方便地实现协作式多任务。在主流的程序语言(如C++、Java、Pascal等)里我们非常少能看到协程的身影,可是如今不少动态脚本语言(Python、Perl)却都提供了协程或与之相似的机制,当中最突出的便是Lua。

        Lua语言实现的协程是一种非对称式(asymmetric)协程,或称半对称式(semi-symmetric)协程,又或干脆就叫半协程(semi-coroutine)。这种协程机制之所以被称为非对称的,是由于它提供了两种传递程序控制权的操作:一种是(重)调用协程(通过coroutine.resume);还有一种是挂起协程并将程序控制权返回给协程的调用者(通过coroutine.yield)。一个非对称协程能够看做是从属于它的调用者的,二者的关系非常相似于例程(routine)与其调用者之间的关系。既然有非对称式协程,当然也就有对称式(symmetric)协程了,它的特点是仅仅有一种传递程序控制权的操作,即将控制权直接传递给指定的协程。以前有这么一种说法,对称式和非对称式协程机制的能力并不等价,但其实非常easy依据前者来实现后者。接下来我们就用代码来证明这个事实。

    --对称式协程库coro.lua

    --代码摘自论文"Coroutines in Lua"
    --www.inf.puc-rio.br/~roberto/docs/corosblp.pdf

    coro = {}
    --coro.main用来标识程序的主函数
    coro.main = function() end
    -- coro.current变量用来标识拥有控制权的协程,
    -- 也即正在运行的当前协程
    coro.current = coro.main

    -- 创建一个新的协程
    function coro.create(f)
       return coroutine.wrap(function(val)
                                return nil,f(val)
                             end)
    end

    -- 把控制权及指定的数据val传给协程k
    function coro.transfer(k,val)
       if coro.current ~= coro.main then
          return coroutine.yield(k,val)
       else
          -- 控制权分派循环
          while k do
             coro.current = k
             if k == coro.main then
                return val
             end
             k,val = k(val)
          end
          error("coroutine ended without transfering control...")
       end
    end

    假设临时还弄不懂上面的程序,没关系,看看怎样使用这个库后再回头分析。以下是使用演示样例:

    require("coro.lua")

    function foo1(n)
       print("1: foo1 received value "..n)
       n = coro.transfer(foo2,n + 10)
       print("2: foo1 received value "..n)
       n = coro.transfer(coro.main,n + 10)
       print("3: foo1 received value "..n)
       coro.transfer(coro.main,n + 10)
    end

    function foo2(n)
       print("1: foo2 received value "..n)
       n = coro.transfer(coro.main,n + 10)
       print("2: foo2 received value "..n)
       coro.transfer(foo1,n + 10)
    end

    function main()
       foo1 = coro.create(foo1)
       foo2 = coro.create(foo2)
       local n = coro.transfer(foo1,0)
       print("1: main received value "..n)
       n = coro.transfer(foo2,n + 10)
       print("2: main received value "..n)
       n = coro.transfer(foo1,n + 10)
       print("3: main received value "..n)
    end

    --把main设为主函数(协程)
    coro.main = main
    --将coro.main设为当前协程
    coro.current = coro.main
    --開始运行主函数(协程)
    coro.main()

    上面的演示样例定义了一个名为main的主函数,整个程序由它而始,也因它而终。为什么须要一个这种主函数呢?上面说了,程序控制权能够在对称式协程之间自由地直接传递,它们之间无所谓谁从属于谁的问题,都处于同一个层级,可是应用程序必须有一个開始点,所以我们定义一个主函数,让它点燃程序运行的导火线。虽说各个协程都是平等的,但做为程序运行原动力的主函数仍然享有特殊的地位(这个世上哪有绝对的平等!),为此我们的库专门用了一个coro.main变量来保存主函数,并且在它运行之前要将它设为当前协程(尽管上面的main实际仅仅是一个普通函数而非一个真正的协程,但这并无太大的关系,以后主函数也被称为主协程)。演示样例运行的结果是:

    1: foo1 received value 0
    1: foo2 received value 10
    1: main received value 20
    2: foo2 received value 30
    2: foo1 received value 40
    2: main received value 50
    3: foo1 received value 60
    3: main received value 70

    协程的运行序列是:main->foo1->foo2->main->foo2->foo1->main->foo1->main。

        coro.transfer(k,val)函数中k是将要接收程序控制权的协程,而val是传递给k的数据。假设当前协程不是主协程,tansfer(k,val)就简单地利用coroutine.yield(k,val)将当前协程挂起并传回两项数据,即程序控制权的下一站和传递给它的数据;否则进入一个控制权分派(dispatch)循环,该循环(重)启动(resume)k协程,等待它运行到挂起(suspend),并依据此时协程传回的数据来决定下一个要(重)启动的协程。从应用演示样例来看,协程与协程之间似乎是用transfer直接传递控制权的,但实际上这个传递还是通过了主协程。每个在主协程里被调用(比較coro.current和coro.main是否同样就可以推断出)的transfer都相当于一个协程管理器,它不断地(重)启动一个协程,将控制权交出去,然后等那个协程挂起时又将控制权收回,然后再(重)启动下一个协程...,这个动作不会停止,除非<1>将(重)启动的协程是主协程;<2>某个协程没有提供控制权的下一个目的地。非常显然,每一轮分派循环開始时都由主协程把握控制权,在循环过程中假设控制权的下一站又是主协程的话就意味着这个当初把控制权交出去的主协程transfer操作应该结束了,所以函数直接返回val从而结束这轮循环。对于情况<2>,由于coro.create(f)创建的协程的体函数(body function)实际是function(val) return nil,f(val) end,所以当函数f的最后一条指令不是transfer时,这个协程终将运行完成并把nil和函数f的返回值一起返回。假设k是这种协程,transfer运行完k,val = k(val)语句后k值就成了nil,这被视为一个错误,由于程序此时没法确定下一个应该(重)启动的协程究竟是谁。所以在对称式模型下,每个协程(当然主协程出外)最后都必须显式地将控制权传递给其它的协程。依据以上分析,应用演示样例的控制权的分派应为:

    第一轮分派: main->foo1->main->foo2->main->main(结束)
    第二轮分派: main->foo2->main->foo1->main->main(结束)
    第三轮分派: main->foo1->main->main(结束)

        由于能够直接指定控制权传递的目标,对称式协程机制拥有极大的自由,但得到这种自由的代价却是牺牲程序结构。假设程序略微复杂一点,那么即使是非常有经验的程序猿也非常难对程序流程有全面而清晰的把握。这非常相似goto语句,它能让程序跳转到不论什么想去的地方,但人们却非常难理解充斥着goto的程序。非对称式协程具有良好的层次化结构关系,(重)启动这些协程与调用一个函数非常相似:被(重)启动的协程得到控制权開始运行,然后挂起(或结束)并将控制权返回给协程调用者,这与计算机先哲们倡导的结构化编程风格全然一致。

        综上所述,Lua提供的非对称式协程不但具有与对称式协程一样强大的能力,并且还能避免程序猿滥用机制写出结构混乱的程序。

  • 相关阅读:
    printcap
    browser-ua
    PHP 开发 APP 接口 学习笔记与总结
    Java实现 LeetCode 72 编辑距离
    Java实现 LeetCode 72 编辑距离
    Java实现 LeetCode 72 编辑距离
    Java实现 LeetCode 71 简化路径
    Java实现 LeetCode 71 简化路径
    Java实现 LeetCode 71 简化路径
    Java实现 LeetCode70 爬楼梯
  • 原文地址:https://www.cnblogs.com/mfrbuaa/p/4295742.html
Copyright © 2011-2022 走看看