美文网首页Lua教程
Lua极简入门(九)——协同程序

Lua极简入门(九)——协同程序

作者: 李小磊_0867 | 来源:发表于2019-11-09 22:54 被阅读0次

    Lua的协同程序和常见的线程相似,可以具有独立的执行流程,包括所需的数据和内存。

    print(coroutine.create(function () end))
    -->> thread: 0000000000706a88
    

    该示例创建了一个协同程序,并将其打印出来,显示为thread...,从这里的显示名看,协调程序确实一个线程。但协调程序和线程相比,也有一些区别;比如Java的多线程,多个线程是可以独立运行,但Lua的协同程序,任意时刻只能运行一个。

    同线程类似,协同程序也有状态,其状态及各个状态的含义参见下表。

    状态 描述
    suspended 挂起,创建时,状态为挂起
    running 运行
    dead 死亡,该状态的协同,不能够被再次调起
    normal 正常,协同A唤起协调B,协调B处于运行状态,此时协调A状态为normal

    使用协同时,需要使用coroutine对象,该对象提供了一些方法用来构建协调程序,该对象共包含7个方法,比较常用的如下所示。

    函数名 参数 返回值 描述
    coroutine.create(f) function,协同程序执行的主函数,一般为匿名函数 协同程序的控制对象,可以理解为线程对象 创建一个新的协同程序,参数描述了协同程序的主要工作流程。
    coroutine.resume(co, val1, ...) 协同对象,多个传入参数,变长 boolean [yield参数|return数据]两种返回,具体描述参见下面章节 启动一个协同程序。根据是否新创建协同和是否由yield挂起,执行方式不同,如果由yield挂起,则resume从挂起位置开始执行
    coroutine.status(co) 协同对象 协同程序的状态 查询协同程序的状态,分为四种。
    coroutine.yield(...) 变长参数 再次resume该协同程序的方法,传入的参数 挂起正在执行的协同程序
    • 创建协同程序
    co = coroutine.create(
            function () 
                print("hello coroutine") 
            end
    )
    print(co)
    -->> thread: 00000000007b88b8
    

    使用coroutine.create方法创建了一个新的携程,其中function采用了匿名方法的形式,该函数描述了该携程的主要工作,本例只打印了一个hello。在实际工作中,采用面向对象概念设计程序时,协同和主流程可以分开书写,function可以以其他类方法引入。

    -- 创建Person对象,并提供了考试及批卷,返回得分的方法
    local Person = {}
    Person.exam = function(score)
        print("考试得分:" .. score)
    end
    
    -- 考试流程中,创建协同程序
    co = coroutine.create(Person.exam)
    
    coroutine.resume(co, 50)
    -->> 考试得分:50
    
    • 协同状态
    co = coroutine.create(
            function ()
                print("hello coroutine")
            end
    )
    
    print(coroutine.status(co))
    -->> suspended
    

    使用coroutine.status方法获取一个协同的状态,本例中因为只创建了一个协同对象,其为挂起状态(suspended),协同有多个状态,如果要展示其四种状态,需要配合其他方法共同完成,会在本节最后的组合中展示这些状态。

    • 协同恢复

    使用coroutine.resume方法启动或者再次启动一个协同。如果协同程序运行过程中,没有任何错误产生,将返回true,如果协同中包含了挂起yield方法时,除了true外,yield中传入的所有参数也都将随 resume函数一同返回,这个特性很重要,可以使用resume+yield组合在多个协同间传递数据;当发生错误时,除了返回false外,还将返回错误信息。

    co = coroutine.create(
            function ()
                print("hello coroutine")
            end
    )
    
    local i = coroutine.resume(co) -- 没有yield,并且程序无错误,返回true
    print(i)
    -->> hello coroutine
    -->> true
    

    当产生错误是,除了false外,代表协同失败,同时还会返回错误信息

    co = coroutine.create(
            function(a, b)
                if b == 0 then
                    error("除数为0")
                end
                local result = a / b
                print("a / b = " .. result)
            end
    )
    
    local i, err = coroutine.resume(co, 12, 0)
    print(i, err)
    -->> false  json2lua.lua:15: 除数为0
    

    返回数据,多个协同间传输数据

    co = coroutine.create(
            function(a, b)
                if b == 0 then
                    error("除数为0")
                end
                local result = a / b
                coroutine.yield("a/b结果", result)
                print("计算结束")
            end
    )
    
    local i, des, result = coroutine.resume(co, 12, 2)
    print(i, des, result)
    print(coroutine.resume(co))
    -->> true   a/b结果   6.0
    -->> 计算结束
    -->> true
    
    • 携程挂起

    当一个协同创建后,其状态为挂起,使用resume可以启动协同程序执行,一旦协同程序开始执行,其他流程都将被挂起,等待执行中的协同程序执行完毕。如果在协同程序执行过程中,需要暂停,等待其他程序执行,之后再恢复并继续执行,那么需要使用到coroutine.yield,该函数可以将当前的协同程序挂起,同时也可以借助yield与外部进行数据通信。

    co = coroutine.create(
            function()
                for i = 1, 2 do
                    print(i)
                    coroutine.yield()
                end
            end
    )
    
    coroutine.resume(co)
    -->> 1
    coroutine.resume(co)
    -->> 2
    coroutine.resume(co)
    -- 此处什么都不打印,因为循环到2时,挂起,再恢复,跳出循环,此时程序执行完毕
    print(coroutine.resume(co)) -- 协同结束,状态为dead,无法再恢复一个死亡的协同,输出错误信息
    -->> false  cannot resume dead coroutine
    

    这个示例演示了协同的挂起,借助resume+yield可以实现更多的功能,在上述的举例中,挂起貌似没有起到太大作用,在这里从另外一个角度思考:如果要下载一个文档,一个协同负责下载和保存文档的任务,并且该协同无法操作UI,直接执行协同,在文件比较大时,可能需要花费许多时间,用户傻傻的等待,可能不知所措,也可能认为程序宕机了,会直接关闭;此时可以借助恢复+挂起机制,实现一个主干线程绘制UI,显示下载的进度。

    -- 下载文件服务对象
    local FileServer = {}
    
    -- 文档下载
    FileServer.download = function()
        -- 文档从1%下载到100%
        local time = os.time()
        local i = 1
        local last = 10
        local downSuccess = false
        while true do
            -- 每隔两秒挂起一次,模拟下载
            if os.time() - time >= 2 then
                if i == last then
                    downSuccess = true
                end
                -- 进度提升10倍
                local tip = "下载进度:" .. math.modf(i * 10) .. "%"
                i = i + 1
                time = os.time()
                coroutine.yield(tip, downSuccess)
            end
            if i > last then
                break
            end
        end
    end
    
    
    -- 主干流程
    local downloadCo = coroutine.create(FileServer.download)
    
    local uiCo = coroutine.create(
            function()
                local ok = false
                repeat
                    local _, tip, ok = coroutine.resume(downloadCo)
                    print(tip)
                    if ok then
                        print("下载完成")
                    end
                until ok
            end
    )
    -- 点击下载按钮执行下载
    coroutine.resume(uiCo)
    -->> 下载进度:10%
    -->> 下载进度:20%
    -->> ...
    -->> 下载进度:100%
    -->> 下载完成
    

    在本例的下载示例中,其核心思想是下载服务中,只完成http下载文件及保存文件行为,并每隔2秒往外部传递当前进度数据,UI控制协同程序接收到数据后,刷新UI显示。使用os.time函数进行间隔挂起协同。

    • 方法组合

    在之前的示例中,并没有看到协同的四种状态,挂起状态很简单,当创建一个协同程序时,其状态就是挂起;运行状态也比较容易理解,当一个协调程序被启动后,其状态为运行;当一个协同程序运行完毕后,其状态为死亡,之前也看到过。

    这里比较难于理解的是normal正常状态,按文档描述:当有两个协同程序时,协同程序A在执行过程中,启动协同程序B,此时协同程序B状态为运行状态,而协同程序A此时既不是运行、也不是挂起,是一种特殊的状态,即为正常状态。

    co = coroutine.create(
            function()
                print("corotine A")
                -- 此时协同co2启动协同co,co处于运行状态,co2处于正常状态
                print("co2 status:", coroutine.status(co2)) 
                coroutine.yield()
                print("corotine A continue run")
            end
    )
    
    co2 = coroutine.create(
            function()
                print("co2 status:", coroutine.status(co2))
                coroutine.resume(co)
                coroutine.resume(co)
            end
    )
    print("co status:", coroutine.status(co)) -- 挂起
    coroutine.resume(co2)
    -- 此时协同co2、co都运行结束,状态为死亡
    print("co2 status:", coroutine.status(co2))
    -- 上述程序输出
    -->> co status: suspended
    -->> co2 status:    running
    -->> corotine A
    -->> co2 status:    normal
    -->> corotine A continue run
    -->> co2 status:    dead
    

    在介绍挂起方法的时候,介绍过resume+yield进行数据交换,主要是从协同中往外传递数据,当协同中使用yield挂起时,方法yield也会有返回值,其返回值,为外部调用resume的传入参数,这种方式可以实现在协同程序运行期间,不断的进行数据交换。

    local co = coroutine.create(
            function(a, b)
                print(a, b) -- 1. 输出 4,5
                local c, d = coroutine.yield(a + 1, b - 1) -- 2. 挂起,向外传递数据 5,4
                print(a, b) -- 4. 继续执行,输出4,5
                print(c, d) -- 5. yield返回的数据为上一个resume的参数,输出 8,9
                return c + d -- 6. 返回 17
            end
    )
    
    print(coroutine.resume(co, 4, 5)) -- 3. 输出 true 5 4
    print(coroutine.resume(co, 8, 9)) -- 7. 输出 true 17
    -->> 4  5
    -->> true   5   4
    -->> 4  5
    -->> 8  9
    -->> true   17
    

    这个例子介绍了resume+yield最后一种传递数据的方式,理解了这些,对于理解协同程序有很大帮助,正是因为Lua协同程序的这些灵活的特点,可以开发出很多特性的功能,前提是需要对这些特性理解清楚,因为灵活的另一面就是难于理解。理解这些,终于可以理解Lua官方文档中关于协同程序一章的示例。

    function foo (a)
        print("foo", a) -- 2. foo 2
        return coroutine.yield(2 * a) -- 3. 挂起  6. resume传入参数"r",作为yield的返回,并作为foo的返回
    end
    
    co = coroutine.create(function(a, b)
        print("co-body", a, b)  -- 1. co-body 1 10
        local r = foo(a + 1) -- 7. r = "r"
        print("co-body", r) -- 8. co-body r
        local r, s = coroutine.yield(a + b, a - b) -- 9. 挂起, 传出 11 -9 12. yield返回 r="x",s="y"
        print("co-body", r, s)  -- 13. co-body x y
        return b, "end"
    end)
    
    print("main", coroutine.resume(co, 1, 10)) -- 4. main true 4
    print("main", coroutine.resume(co, "r")) -- 5. 启动,并传入 "r" -- 10. main true 11 -9
    print("main", coroutine.resume(co, "x", "y")) -- 11. 启动,传入 "x" "y" 14. main true 10 end
    print("main", coroutine.resume(co, "x", "y")) -- main false error
    -->> co-body    1   10
    -->> foo    2
    -->> main   true    4
    -->> co-body    r
    -->> main   true    11  -9
    -->> co-body    x   y
    -->> main   true    10  end
    -->> main   false   cannot resume dead coroutine
    
    1. 启动协同程序时,resume至少携带一个参数(协同),当其携带多个参数时,

      ​ a. 当协同程序刚创建并处于挂起状态时,此时resume的参数将作为协同函数的参数传入

      ​ b. 当协同程序由yield挂起时,resume的参数将作为yield函数的返回值传入协同程序

    2. resume返回值情况

      ​ a. 当协同程序由yield挂起时,此时返回值格式为boolean [yield函数参数],布尔值表示当前协同程 序执行的状态,true为无错误,yield函数的参数将作为返回值传出,可以使用该类型往主程序传递 数据

      ​ b. 当协同程序结束时,返回值格式为boolean [return数据],布尔值表示协同程序执行状态;return 数据为协同方法的返回值,如果没有返回值,则值具有布尔值。

    相关文章

      网友评论

        本文标题:Lua极简入门(九)——协同程序

        本文链接:https://www.haomeiwen.com/subject/pzhobctx.html