閱讀1009 返回首頁    go 阿裏雲 go 技術社區[雲棲]


LUA 協程 Coroutine

協程 Coroutine

協程(coroutine)並不是 Lua 獨有的概念,如果讓我用一句話概括,那麼大概就是:一種能夠在運行途中主動中斷,並且能夠從中斷處恢複運行的特殊函數。(嗯,其實不是函數。)

舉個最原始的例子:

下麵給出一個最簡單的 Lua 中 coroutine 的用法演示:

function greet()
    print "hello world"
end

co = coroutine.create(greet) -- 創建 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

協程在創建時,需要把協程體函數傳遞給創建函數 create。新創建的協程處於 suspended 狀態,可以使用 resume 讓其運行,全部執行完成後協程處於 dead 狀態。如果嚐試 resume 一個 dead 狀態的,則可以從 resume 返回值上看出執行失敗。另外你還可以注意到 Lua 中協程(coroutine)的變量類型其實叫做「thread」Orz...

乍一看可能感覺和線程沒什麼兩樣,但需要注意的是 resume 函數隻有在 greet 函數「返回」後才會返回(所以說協程像函數)。

 函數執行的中斷與再開

單從上麵這個例子,我們似乎可以得出結論:協程果然就是某種坑爹的函數調用方式啊。然而,協程的真正魅力來自於 resume 和 yield 這對好基友之間的羈絆。

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

開始或恢複執行協程 co。

如果是開始執行,val1 及之後的值都作為參數傳遞給協程體函數;如果是恢複執行,val1 及之後的值都作為 yield 的返回值傳遞。

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

當然,如果是協程體函數執行完畢 return 而不是 yield,那麼 resume 第一個返回值後跟著的就是其返回值。

函數 coroutine.yield(...)

中斷協程的執行,使得開啟該協程的 coroutine.resume 返回。再度調用 coroutine.resume 時,會從該 yield 處恢複執行。

當然,yield 的所有參數都會作為 resume 第一個返回值後的返回值返回。

OK,總結一下:當 co = coroutine.create(f) 時,yield 和 resume 的關係如下圖:

How coroutine makes life easier

如果要求給某個怪寫一個 AI:先向右走 30 幀,然後隻要玩家進入視野就往反方向逃 15 幀。該怎麼寫?

傳統做法

經典的純狀態機做法。

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

-- 切換狀態
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

-- 等待玩家進入視野
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

協程做法

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

    while true do
        -- 等待玩家進入視野
        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

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

額外說一句,從 wait 函數可以看出,Lua 的協程並不要求一定要從協程體函數中調用 yield,這是和 Python 的一個區別。

協同程序(coroutine,這裏簡稱協程)是一種類似於線程(thread)的東西,它擁有自己獨立的棧、局部變量和指令指針,可以跟其他協程共享全局變量和其他一些數據,並且具有一種掛起(yield)中斷協程主函數運行,下一次激活恢複協程會在上一次中斷的地方繼續執行(resume)協程主函數的控製機製。

Lua 把關於協程的所有函數放在一個名為 “coroutine” 的 table 裏,coroutine 裏具有以下幾個內置函數:

-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 - 創建協程

函數 coroutine.create 用於創建一個新的協程,它隻有一個以函數形式傳入的參數,該函數是協程的主函數,它的代碼是協程所需執行的內容

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

當創建完一個協程後,會返回一個類型為 thread 的對象,但並不會馬上啟動運行協程主函數,協程的初始狀態是處於掛起狀態

coroutine.status - 查看協程狀態

協程有 4 種狀態,分別是:掛起(suspended)、運行(running)、死亡(dead)和正常(normal),可以通過 coroutine.status 來輸出查看協程當前的狀態。

print(coroutine.status(co))

coroutine.resume - 執行協程

函數 coroutine.resume 用於啟動或再次啟動一個協程的執行

coroutine.resume(co)

協程被調用執行後,其狀態會由掛起(suspended)改為運行(running)。不過當協程主函數全部運行完之後,它就變為死亡(dead)狀態。

傳遞給 resume 的額外參數都被看作是協程主函數的參數

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

協程主函數執行完時,它的主函數所返回的值都將作為對應 resume 的返回值

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

coroutine.yield - 中斷協程運行

coroutine.yield 函數可以讓一個運行中的協程中斷掛起

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)
上麵第一個 resume 喚醒執行協程主函數代碼,直到第一個 yield。第二個 resume 激活被掛起的協程,並從上一次協程被中斷 yield 的位置繼續執行協程主函數代碼,直到再次遇到 yield 或程序結束。

resume 執行完協程主函數或者中途被掛起(yield)時,會有返回值返回,第一個值是 true,表示執行沒有錯誤。如果是被 yield 掛起暫停,yield 函數有參數傳入的話,這些參數會接著第一個值後麵一並返回

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

以 coroutine.wrap 的方式創建協程

跟 coroutine.create 一樣,函數 coroutine.wrap 也是創建一個協程,但是它並不返回一個類型為 thread 的對象,而是返回一個函數。每當調用這個返回函數,都會執行協程主函數運行。所有傳入這個函數的參數等同於傳入 coroutine.resume 的參數。 coroutine.wrap 會返回所有應該由除第一個(錯誤代碼的那個布爾量) 之外的由 coroutine.resume 返回的值。 和 coroutine.resume 不同之處在於, coroutine.wrap 不會返回錯誤代碼,無法檢測出運行時的錯誤,也無法檢查 wrap 所創建的協程的狀態

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 - 返回正在運行中的協程

函數 coroutine.running 用於返回正在運行中的協程,如果沒有協程運行,則返回 nil

print(coroutine.running())

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

print(coroutine.running())

resume-yield 交互

下麵代碼放在一個 lua 文件裏運行,隨便輸入一些字符後按回車,則會返回輸出剛才輸入的內容

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)

最後更新:2017-07-19 17:02:46

  上一篇:go  物理備份後主從複製出現的問題
  下一篇:go  四甲基二苯基三矽氧烷和苯基含氫矽油