Lua教程

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

2019-11-09  本文已影响0人  李小磊_0867

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 数据为协同方法的返回值,如果没有返回值,则值具有布尔值。

上一篇下一篇

猜你喜欢

热点阅读