Lua CallbackHell優化

概述

在非同步操作中,常常要使用回調。但是,回調的嵌套常常會導致邏輯混亂,一步錯步步錯,難以維護。在Lua中,可以使用協程進行優化。

問題分析

模擬一個回合制遊戲攻擊過程

local function PlayAnim(anim, cb)
    print("開始播放 " .. anim)
    os.execute("sleep " .. 1)
    print("播放完成 " .. anim)
    cb()
end

local function Main()
    print("行動開始")
    PlayAnim("移動到目標動畫",function() 
        print("開始攻擊")
        PlayAnim("攻擊動畫",function() 
            print("返回到原位置")
            PlayAnim("返回動畫",function() 
                print("行動結束")
            end)
        end)
    end)
end

Main()

輸出:
image

  • 可以看到非同步回調的嵌套導致程式碼結構混亂

Lua協程

簡介

  • Lua 協同程式(coroutine)與執行緒比較類似:擁有獨立的堆棧,獨立的局部變數,獨立的指令指針,同時又與其它協同程式共享全局變數和其它大部分東西。
  • 執行緒與協同程式的主要區別在於,一個具有多個執行緒的程式可以同時運行幾個執行緒,而協同程式卻需要彼此協作的運行。在任一指定時刻只有一個協同程式在運行,並且這個正在運行的協同程式只有在明確的被要求掛起的時候才會被掛起。
  • 協同程式有點類似同步的多執行緒,在等待同一個執行緒鎖的幾個執行緒有點類似協同。
  • coroutine在底層實現就是一個執行緒。

API

API 說明
coroutine.create() 創建 coroutine,返回 coroutine, 參數是一個函數,當和 resume 配合使用的時候就喚醒函數調用
coroutine.resume() 重啟 coroutine,和 create 配合使用
coroutine.yield() 掛起 coroutine,將 coroutine 設置為掛起狀態,這個和 resume 配合使用能有很多有用的效果
coroutine.status() 查看 coroutine 的狀態。註:coroutine 的狀態有三種:dead,suspended,running
coroutine.wrap() 創建 coroutine,返回一個函數,一旦你調用這個函數,就進入 coroutine,和 create 功能重複
coroutine.running() 返回正在跑的 coroutine,一個 coroutine 就是一個執行緒,當使用running的時候,就是返回一個 corouting 的執行緒號

詳細介紹

請參考跳轉鏈接:菜鳥教程-協程

解決方案

function AsyncFunc(func)
    return function(...)
        local current_co = coroutine.running()
        local ret, res = false, nil
        local function cb(...)
            if not coroutine.resume(current_co, ...) then
                ret = true
                res = ...
            end
        end
        local params = table.pack(...)
        table.insert(params, cb)
        func(table.unpack(params))
        if not ret then
            res = coroutine.yield()
        end
        return res
    end
end

function BeginTask(func, ...)
    local t = coroutine.create(func)
    coroutine.resume(t, ...)
end

local function PlayAnim(anim, cb)
    print("開始播放 " .. anim)
    os.execute("sleep " .. 1)
    print("播放完成 " .. anim)
    cb()
end

local AsyncPlayAnim = AsyncFunc(PlayAnim)
local function Main()
    print("行動開始")
    AsyncPlayAnim("移動到目標動畫")
    print("開始攻擊")
    AsyncPlayAnim("攻擊動畫")
    print("返回到原位置")
    AsyncPlayAnim("返回動畫")
    print("行動結束")
end

BeginTask(Main)

輸出:
image

  • 用AsyncFunc和BeginTask分別對非同步函數和協程創建和運行做了封裝
  • 注意:不能在主協程中運行AsyncFunc,用BeginTask開啟一個新的協程運行Main
Tags: