5. 協(xié)程 Coroutine

2018-02-24 16:10 更新

協(xié)程(coroutine)并不是 Lua 獨(dú)有的概念,如果讓我用一句話概括,那么大概就是:一種能夠在運(yùn)行途中主動(dòng)中斷,并且能夠從中斷處恢復(fù)運(yùn)行的特殊函數(shù)。(嗯,其實(shí)不是函數(shù)。)

舉個(gè)最原始的例子:

下面給出一個(gè)最簡(jiǎn)單的 Lua 中 coroutine 的用法演示:

function greet()
    print "hello world"
end

co = coroutine.create(greet) -- 創(chuàng)建 coroutine

print(coroutine.status(co))  -- 輸出 suspended
print(coroutine.resume(co))  -- 輸出 hello world
                             -- 輸出 true (resume 的返回值)
print(coroutine.status(co))  -- 輸出 dead
print(coroutine.resume(co))  -- 輸出 false    cannot resume dead coroutine (resume 的返回值)
print(type(co))              -- 輸出 thread

協(xié)程在創(chuàng)建時(shí),需要把協(xié)程體函數(shù)傳遞給創(chuàng)建函數(shù) create。新創(chuàng)建的協(xié)程處于 suspended 狀態(tài),可以使用 resume 讓其運(yùn)行,全部執(zhí)行完成后協(xié)程處于 dead 狀態(tài)。如果嘗試 resume 一個(gè) dead 狀態(tài)的,則可以從 resume 返回值上看出執(zhí)行失敗。另外你還可以注意到 Lua 中協(xié)程(coroutine)的變量類(lèi)型其實(shí)叫做「thread」Orz...

乍一看可能感覺(jué)和線程沒(méi)什么兩樣,但需要注意的是 resume 函數(shù)只有在 greet 函數(shù)「返回」后才會(huì)返回(所以說(shuō)協(xié)程像函數(shù))。

 函數(shù)執(zhí)行的中斷與再開(kāi)

單從上面這個(gè)例子,我們似乎可以得出結(jié)論:協(xié)程果然就是某種坑爹的函數(shù)調(diào)用方式啊。然而,協(xié)程的真正魅力來(lái)自于 resume 和 yield 這對(duì)好基友之間的羈絆。

函數(shù) coroutine.resume(co[, val1, ...])

開(kāi)始或恢復(fù)執(zhí)行協(xié)程 co。

如果是開(kāi)始執(zhí)行,val1 及之后的值都作為參數(shù)傳遞給協(xié)程體函數(shù);如果是恢復(fù)執(zhí)行,val1 及之后的值都作為 yield 的返回值傳遞。

第一個(gè)返回值(還記得 Lua 可以返回多個(gè)值嗎?)為表示執(zhí)行成功與否的布爾值。如果成功,之后的返回值是 yield 的參數(shù);如果失敗,第二個(gè)返回值為失敗的原因(Lua 的很多函數(shù)都采用這種錯(cuò)誤處理方式)。

當(dāng)然,如果是協(xié)程體函數(shù)執(zhí)行完畢 return 而不是 yield,那么 resume 第一個(gè)返回值后跟著的就是其返回值。

函數(shù) coroutine.yield(...)

中斷協(xié)程的執(zhí)行,使得開(kāi)啟該協(xié)程的 coroutine.resume 返回。再度調(diào)用 coroutine.resume 時(shí),會(huì)從該 yield 處恢復(fù)執(zhí)行。

當(dāng)然,yield 的所有參數(shù)都會(huì)作為 resume 第一個(gè)返回值后的返回值返回。

OK,總結(jié)一下:當(dāng) co = coroutine.create(f) 時(shí),yield 和 resume 的關(guān)系如下圖:

How coroutine makes life easier

如果要求給某個(gè)怪寫(xiě)一個(gè) AI:先向右走 30 幀,然后只要玩家進(jìn)入視野就往反方向逃 15 幀。該怎么寫(xiě)?

傳統(tǒng)做法

經(jīng)典的純狀態(tài)機(jī)做法。

-- 每幀的邏輯
function Monster:frame()
    self:state_func()
    self.state_frame_count = self.state_frame_count + 1
end

-- 切換狀態(tài)
function Monster:set_next_state(state)
    self.state_func = state
    self.state_frame_count = 0
end

-- 首先向右走 30 幀
function Monster:state_walk_1()
    local frame = self.state_frame_count
    self:walk(DIRECTION_RIGHT)
    if frame > 30 then
        self:set_next_state(state_wait_for_player)
    end
end

-- 等待玩家進(jìn)入視野
function Monster:state_wait_for_player()
    if self:get_distance(player) < self.range then
        self.direction = -self:get_direction_to(player)
        self:set_next_state(state_walk_2)
    end
end

-- 向反方向走 15 幀
function Monster:state_walk_2()
    local frame = self.state_frame_count;
    self:walk(self.direction)
    if frame > 15 then
        self:set_next_state(state_wait_for_player)
    end
end

協(xié)程做法

-- 每幀的邏輯
function Monster:frame()
    -- 首先向右走 30 幀
    for i = 1, 30 do
        self:walk(DIRECTION_RIGHT)
        self:wait()
    end

    while true do
        -- 等待玩家進(jìn)入視野
        while self:get_distance(player) >= self.range do
            self:wait()
        end

        -- 向反方向走 15 幀
        self.direction = -self:get_direction_to(player)
        for i = 1, 15 do
            self:walk(self.direction)
            self:wait()
        end
    end
end

-- 該幀結(jié)束
function Monster:wait()
    coroutine.yield()
end

額外說(shuō)一句,從 wait 函數(shù)可以看出,Lua 的協(xié)程并不要求一定要從協(xié)程體函數(shù)中調(diào)用 yield,這是和 Python 的一個(gè)區(qū)別。

協(xié)同程序(coroutine,這里簡(jiǎn)稱(chēng)協(xié)程)是一種類(lèi)似于線程(thread)的東西,它擁有自己獨(dú)立的棧、局部變量和指令指針,可以跟其他協(xié)程共享全局變量和其他一些數(shù)據(jù),并且具有一種掛起(yield)中斷協(xié)程主函數(shù)運(yùn)行,下一次激活恢復(fù)協(xié)程會(huì)在上一次中斷的地方繼續(xù)執(zhí)行(resume)協(xié)程主函數(shù)的控制機(jī)制。

Lua 把關(guān)于協(xié)程的所有函數(shù)放在一個(gè)名為 “coroutine” 的 table 里,coroutine 里具有以下幾個(gè)內(nèi)置函數(shù):

-coroutine-yield [function: builtin#34]
|         -wrap [function: builtin#37]
|         -status [function: builtin#31]
|         -resume [function: builtin#35]
|         -running [function: builtin#32]
|         -create [function: builtin#33]

coroutine.create - 創(chuàng)建協(xié)程

函數(shù) coroutine.create 用于創(chuàng)建一個(gè)新的協(xié)程,它只有一個(gè)以函數(shù)形式傳入的參數(shù),該函數(shù)是協(xié)程的主函數(shù),它的代碼是協(xié)程所需執(zhí)行的內(nèi)容

co = coroutine.create(function() 
    io.write("coroutine create!\n") 
end)
print(co)

當(dāng)創(chuàng)建完一個(gè)協(xié)程后,會(huì)返回一個(gè)類(lèi)型為 thread 的對(duì)象,但并不會(huì)馬上啟動(dòng)運(yùn)行協(xié)程主函數(shù),協(xié)程的初始狀態(tài)是處于掛起狀態(tài)

coroutine.status - 查看協(xié)程狀態(tài)

協(xié)程有 4 種狀態(tài),分別是:掛起(suspended)、運(yùn)行(running)、死亡(dead)和正常(normal),可以通過(guò) coroutine.status 來(lái)輸出查看協(xié)程當(dāng)前的狀態(tài)。

print(coroutine.status(co))

coroutine.resume - 執(zhí)行協(xié)程

函數(shù) coroutine.resume 用于啟動(dòng)或再次啟動(dòng)一個(gè)協(xié)程的執(zhí)行

coroutine.resume(co)

協(xié)程被調(diào)用執(zhí)行后,其狀態(tài)會(huì)由掛起(suspended)改為運(yùn)行(running)。不過(guò)當(dāng)協(xié)程主函數(shù)全部運(yùn)行完之后,它就變?yōu)樗劳觯╠ead)狀態(tài)。

傳遞給 resume 的額外參數(shù)都被看作是協(xié)程主函數(shù)的參數(shù)

co = coroutine.create(function(a, b, c)
    print("co", a, b, c)
end)
coroutine.resume(co, 1, 2, 3)

協(xié)程主函數(shù)執(zhí)行完時(shí),它的主函數(shù)所返回的值都將作為對(duì)應(yīng) resume 的返回值

co = coroutine.create(function()
    return 3, 4
end)
print(coroutine.resume(co))

coroutine.yield - 中斷協(xié)程運(yùn)行

coroutine.yield 函數(shù)可以讓一個(gè)運(yùn)行中的協(xié)程中斷掛起

co = coroutine.create(function()
    for i = 1, 3 do
        print("before coroutine yield", i)
        coroutine.yield()
        print("after coroutine yield", i)
    end
end)
coroutine.resume(co)

coroutine.resume(co) 上面第一個(gè) resume 喚醒執(zhí)行協(xié)程主函數(shù)代碼,直到第一個(gè) yield。第二個(gè) resume 激活被掛起的協(xié)程,并從上一次協(xié)程被中斷 yield 的位置繼續(xù)執(zhí)行協(xié)程主函數(shù)代碼,直到再次遇到 yield 或程序結(jié)束。

resume 執(zhí)行完協(xié)程主函數(shù)或者中途被掛起(yield)時(shí),會(huì)有返回值返回,第一個(gè)值是 true,表示執(zhí)行沒(méi)有錯(cuò)誤。如果是被 yield 掛起暫停,yield 函數(shù)有參數(shù)傳入的話,這些參數(shù)會(huì)接著第一個(gè)值后面一并返回

co = coroutine.create(function(a, b, c)
    coroutine.yield(a, b, c)
end)
print(coroutine.resume(co, 1, 2, 3))

以 coroutine.wrap 的方式創(chuàng)建協(xié)程

跟 coroutine.create 一樣,函數(shù) coroutine.wrap 也是創(chuàng)建一個(gè)協(xié)程,但是它并不返回一個(gè)類(lèi)型為 thread 的對(duì)象,而是返回一個(gè)函數(shù)。每當(dāng)調(diào)用這個(gè)返回函數(shù),都會(huì)執(zhí)行協(xié)程主函數(shù)運(yùn)行。所有傳入這個(gè)函數(shù)的參數(shù)等同于傳入 coroutine.resume 的參數(shù)。 coroutine.wrap 會(huì)返回所有應(yīng)該由除第一個(gè)(錯(cuò)誤代碼的那個(gè)布爾量) 之外的由 coroutine.resume 返回的值。 和 coroutine.resume 不同之處在于, coroutine.wrap 不會(huì)返回錯(cuò)誤代碼,無(wú)法檢測(cè)出運(yùn)行時(shí)的錯(cuò)誤,也無(wú)法檢查 wrap 所創(chuàng)建的協(xié)程的狀態(tài)

function wrap(param)
    print("Before yield", param)
    obtain = coroutine.yield()
    print("After yield", obtain)
    return 3
end
resumer = coroutine.wrap(wrap) 

print(resumer(1))

print(resumer(2))

coroutine.running - 返回正在運(yùn)行中的協(xié)程

函數(shù) coroutine.running 用于返回正在運(yùn)行中的協(xié)程,如果沒(méi)有協(xié)程運(yùn)行,則返回 nil

print(coroutine.running())

co = coroutine.create(function() 
    print(coroutine.running())
    print(coroutine.running() == co)
end)
coroutine.resume(co)

print(coroutine.running())

resume-yield 交互

下面代碼放在一個(gè) lua 文件里運(yùn)行,隨便輸入一些字符后按回車(chē),則會(huì)返回輸出剛才輸入的內(nèi)容

function receive(prod)
    local status, value = coroutine.resume(prod)
    return value
end

function send(x)
    coroutine.yield(x)
end

function producer()
    return coroutine.create(function()
        while true do
            local x = io.read()
            send(x)
        end
    end)
end

function filter(prod)
    return coroutine.create(function()
--      for line = 1, math.huge do
        for line = 1, 5 do
            local x = receive(prod)
            x = string.format("%5d Enter is %s", line, x)
            send(x)
        end
    end)
end

function consumer(prod)
--  repeat
--      local x = receive(prod)
--      print(type(x))
--      if x then
--          io.write(x, "\n")
--      end
--  until x == nil 
    while true do
        local obtain = receive(prod)
        if obtain then
            io.write(obtain, "\n\n")
        else
            break
        end
    end
end

p = producer()
f = filter(p)
consumer(f)
以上內(nèi)容是否對(duì)您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號(hào)
微信公眾號(hào)

編程獅公眾號(hào)