Lua極簡入門(九)——協(xié)同程序

Lua的協(xié)同程序和常見的線程相似,可以具有獨立的執(zhí)行流程,包括所需的數(shù)據(jù)和內(nèi)存。

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

該示例創(chuàng)建了一個協(xié)同程序,并將其打印出來,顯示為thread...,從這里的顯示名看,協(xié)調(diào)程序確實一個線程。但協(xié)調(diào)程序和線程相比,也有一些區(qū)別;比如Java的多線程,多個線程是可以獨立運行,但Lua的協(xié)同程序,任意時刻只能運行一個。

同線程類似,協(xié)同程序也有狀態(tài),其狀態(tài)及各個狀態(tài)的含義參見下表。

狀態(tài) 描述
suspended 掛起,創(chuàng)建時,狀態(tài)為掛起
running 運行
dead 死亡,該狀態(tài)的協(xié)同,不能夠被再次調(diào)起
normal 正常,協(xié)同A喚起協(xié)調(diào)B,協(xié)調(diào)B處于運行狀態(tài),此時協(xié)調(diào)A狀態(tài)為normal

使用協(xié)同時,需要使用coroutine對象,該對象提供了一些方法用來構(gòu)建協(xié)調(diào)程序,該對象共包含7個方法,比較常用的如下所示。

函數(shù)名 參數(shù) 返回值 描述
coroutine.create(f) function,協(xié)同程序執(zhí)行的主函數(shù),一般為匿名函數(shù) 協(xié)同程序的控制對象,可以理解為線程對象 創(chuàng)建一個新的協(xié)同程序,參數(shù)描述了協(xié)同程序的主要工作流程。
coroutine.resume(co, val1, ...) 協(xié)同對象,多個傳入?yún)?shù),變長 boolean [yield參數(shù)|return數(shù)據(jù)]兩種返回,具體描述參見下面章節(jié) 啟動一個協(xié)同程序。根據(jù)是否新創(chuàng)建協(xié)同和是否由yield掛起,執(zhí)行方式不同,如果由yield掛起,則resume從掛起位置開始執(zhí)行
coroutine.status(co) 協(xié)同對象 協(xié)同程序的狀態(tài) 查詢協(xié)同程序的狀態(tài),分為四種。
coroutine.yield(...) 變長參數(shù) 再次resume該協(xié)同程序的方法,傳入的參數(shù) 掛起正在執(zhí)行的協(xié)同程序
  • 創(chuàng)建協(xié)同程序
co = coroutine.create(
        function () 
            print("hello coroutine") 
        end
)
print(co)
-->> thread: 00000000007b88b8

使用coroutine.create方法創(chuàng)建了一個新的攜程,其中function采用了匿名方法的形式,該函數(shù)描述了該攜程的主要工作,本例只打印了一個hello。在實際工作中,采用面向?qū)ο蟾拍钤O(shè)計程序時,協(xié)同和主流程可以分開書寫,function可以以其他類方法引入。

-- 創(chuàng)建Person對象,并提供了考試及批卷,返回得分的方法
local Person = {}
Person.exam = function(score)
    print("考試得分:" .. score)
end

-- 考試流程中,創(chuàng)建協(xié)同程序
co = coroutine.create(Person.exam)

coroutine.resume(co, 50)
-->> 考試得分:50
  • 協(xié)同狀態(tài)
co = coroutine.create(
        function ()
            print("hello coroutine")
        end
)

print(coroutine.status(co))
-->> suspended

使用coroutine.status方法獲取一個協(xié)同的狀態(tài),本例中因為只創(chuàng)建了一個協(xié)同對象,其為掛起狀態(tài)(suspended),協(xié)同有多個狀態(tài),如果要展示其四種狀態(tài),需要配合其他方法共同完成,會在本節(jié)最后的組合中展示這些狀態(tài)。

  • 協(xié)同恢復(fù)

使用coroutine.resume方法啟動或者再次啟動一個協(xié)同。如果協(xié)同程序運行過程中,沒有任何錯誤產(chǎn)生,將返回true,如果協(xié)同中包含了掛起yield方法時,除了true外,yield中傳入的所有參數(shù)也都將隨 resume函數(shù)一同返回,這個特性很重要,可以使用resume+yield組合在多個協(xié)同間傳遞數(shù)據(jù);當(dāng)發(fā)生錯誤時,除了返回false外,還將返回錯誤信息。

co = coroutine.create(
        function ()
            print("hello coroutine")
        end
)

local i = coroutine.resume(co) -- 沒有yield,并且程序無錯誤,返回true
print(i)
-->> hello coroutine
-->> true

當(dāng)產(chǎn)生錯誤是,除了false外,代表協(xié)同失敗,同時還會返回錯誤信息

co = coroutine.create(
        function(a, b)
            if b == 0 then
                error("除數(shù)為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: 除數(shù)為0

返回數(shù)據(jù),多個協(xié)同間傳輸數(shù)據(jù)

co = coroutine.create(
        function(a, b)
            if b == 0 then
                error("除數(shù)為0")
            end
            local result = a / b
            coroutine.yield("a/b結(jié)果", result)
            print("計算結(jié)束")
        end
)

local i, des, result = coroutine.resume(co, 12, 2)
print(i, des, result)
print(coroutine.resume(co))
-->> true   a/b結(jié)果   6.0
-->> 計算結(jié)束
-->> true
  • 攜程掛起

當(dāng)一個協(xié)同創(chuàng)建后,其狀態(tài)為掛起,使用resume可以啟動協(xié)同程序執(zhí)行,一旦協(xié)同程序開始執(zhí)行,其他流程都將被掛起,等待執(zhí)行中的協(xié)同程序執(zhí)行完畢。如果在協(xié)同程序執(zhí)行過程中,需要暫停,等待其他程序執(zhí)行,之后再恢復(fù)并繼續(xù)執(zhí)行,那么需要使用到coroutine.yield,該函數(shù)可以將當(dāng)前的協(xié)同程序掛起,同時也可以借助yield與外部進(jìn)行數(shù)據(jù)通信。

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)
-- 此處什么都不打印,因為循環(huán)到2時,掛起,再恢復(fù),跳出循環(huán),此時程序執(zhí)行完畢
print(coroutine.resume(co)) -- 協(xié)同結(jié)束,狀態(tài)為dead,無法再恢復(fù)一個死亡的協(xié)同,輸出錯誤信息
-->> false  cannot resume dead coroutine

這個示例演示了協(xié)同的掛起,借助resume+yield可以實現(xiàn)更多的功能,在上述的舉例中,掛起貌似沒有起到太大作用,在這里從另外一個角度思考:如果要下載一個文檔,一個協(xié)同負(fù)責(zé)下載和保存文檔的任務(wù),并且該協(xié)同無法操作UI,直接執(zhí)行協(xié)同,在文件比較大時,可能需要花費許多時間,用戶傻傻的等待,可能不知所措,也可能認(rèn)為程序宕機(jī)了,會直接關(guān)閉;此時可以借助恢復(fù)+掛起機(jī)制,實現(xiàn)一個主干線程繪制UI,顯示下載的進(jìn)度。

-- 下載文件服務(wù)對象
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
            -- 進(jìn)度提升10倍
            local tip = "下載進(jìn)度:" .. 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
)
-- 點擊下載按鈕執(zhí)行下載
coroutine.resume(uiCo)
-->> 下載進(jìn)度:10%
-->> 下載進(jìn)度:20%
-->> ...
-->> 下載進(jìn)度:100%
-->> 下載完成

在本例的下載示例中,其核心思想是下載服務(wù)中,只完成http下載文件及保存文件行為,并每隔2秒往外部傳遞當(dāng)前進(jìn)度數(shù)據(jù),UI控制協(xié)同程序接收到數(shù)據(jù)后,刷新UI顯示。使用os.time函數(shù)進(jìn)行間隔掛起協(xié)同。

  • 方法組合

在之前的示例中,并沒有看到協(xié)同的四種狀態(tài),掛起狀態(tài)很簡單,當(dāng)創(chuàng)建一個協(xié)同程序時,其狀態(tài)就是掛起;運行狀態(tài)也比較容易理解,當(dāng)一個協(xié)調(diào)程序被啟動后,其狀態(tài)為運行;當(dāng)一個協(xié)同程序運行完畢后,其狀態(tài)為死亡,之前也看到過。

這里比較難于理解的是normal正常狀態(tài),按文檔描述:當(dāng)有兩個協(xié)同程序時,協(xié)同程序A在執(zhí)行過程中,啟動協(xié)同程序B,此時協(xié)同程序B狀態(tài)為運行狀態(tài),而協(xié)同程序A此時既不是運行、也不是掛起,是一種特殊的狀態(tài),即為正常狀態(tài)。

co = coroutine.create(
        function()
            print("corotine A")
            -- 此時協(xié)同co2啟動協(xié)同co,co處于運行狀態(tài),co2處于正常狀態(tài)
            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)
-- 此時協(xié)同co2、co都運行結(jié)束,狀態(tài)為死亡
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進(jìn)行數(shù)據(jù)交換,主要是從協(xié)同中往外傳遞數(shù)據(jù),當(dāng)協(xié)同中使用yield掛起時,方法yield也會有返回值,其返回值,為外部調(diào)用resume的傳入?yún)?shù),這種方式可以實現(xiàn)在協(xié)同程序運行期間,不斷的進(jìn)行數(shù)據(jù)交換。

local co = coroutine.create(
        function(a, b)
            print(a, b) -- 1. 輸出 4,5
            local c, d = coroutine.yield(a + 1, b - 1) -- 2. 掛起,向外傳遞數(shù)據(jù) 5,4
            print(a, b) -- 4. 繼續(xù)執(zhí)行,輸出4,5
            print(c, d) -- 5. yield返回的數(shù)據(jù)為上一個resume的參數(shù),輸出 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最后一種傳遞數(shù)據(jù)的方式,理解了這些,對于理解協(xié)同程序有很大幫助,正是因為Lua協(xié)同程序的這些靈活的特點,可以開發(fā)出很多特性的功能,前提是需要對這些特性理解清楚,因為靈活的另一面就是難于理解。理解這些,終于可以理解Lua官方文檔中關(guān)于協(xié)同程序一章的示例。

function foo (a)
    print("foo", a) -- 2. foo 2
    return coroutine.yield(2 * a) -- 3. 掛起  6. resume傳入?yún)?shù)"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. 啟動協(xié)同程序時,resume至少攜帶一個參數(shù)(協(xié)同),當(dāng)其攜帶多個參數(shù)時,

    ? a. 當(dāng)協(xié)同程序剛創(chuàng)建并處于掛起狀態(tài)時,此時resume的參數(shù)將作為協(xié)同函數(shù)的參數(shù)傳入

    ? b. 當(dāng)協(xié)同程序由yield掛起時,resume的參數(shù)將作為yield函數(shù)的返回值傳入?yún)f(xié)同程序

  2. resume返回值情況

    ? a. 當(dāng)協(xié)同程序由yield掛起時,此時返回值格式為boolean [yield函數(shù)參數(shù)],布爾值表示當(dāng)前協(xié)同程 序執(zhí)行的狀態(tài),true為無錯誤,yield函數(shù)的參數(shù)將作為返回值傳出,可以使用該類型往主程序傳遞 數(shù)據(jù)

    ? b. 當(dāng)協(xié)同程序結(jié)束時,返回值格式為boolean [return數(shù)據(jù)],布爾值表示協(xié)同程序執(zhí)行狀態(tài);return 數(shù)據(jù)為協(xié)同方法的返回值,如果沒有返回值,則值具有布爾值。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容