lua服務執行過程中協程的掛起和重新喚醒

lua服務在執行回調函數的過程中,調用某些函數會掛起協程,比如skynet.call, skynet.ret, skynet.response等等,這些函數把協程掛起後,如何喚醒呢?

本文將對所有調用coroutine.yield的API的喚醒做下分析。(比較拗口,找不到更好的表達方式了)

skynet.call

function skynet.call(addr, typename, ...)
    local p = proto[typename]
    local session = c.send(addr, p.id , nil , p.pack(...))
    if session == nil then
        error("call to invalid address " .. skynet.address(addr))
    end
    return p.unpack(yield_call(addr, session))
end

功能:發起了一次 RPC ,並阻塞等待迴應。

喚醒方式:目標服務調用skynet.ret, 返回一個PTYPE_RESPONSE類型的消息,在raw_dispatch_message函數內,會專門對這類消息做特殊處理,從之前緩存的表(key是session)中取出co, 然後再resume。

詳細過程可以參考這篇文章:skynet.call流程

skynet.rawcall

function skynet.rawcall(addr, typename, msg, sz)
    local p = proto[typename]
    local session = assert(c.send(addr, p.id , nil , msg, sz), "call to invalid address")
    return yield_call(addr, session)
end

它和 skynet.call 功能類似(也是阻塞 API)。但發送時不經過 pack 打包流程,收到迴應後,也不走 unpack 流程。

所以其喚醒方式同skynet.call。

skynet.ret

function skynet.ret(msg, sz)
    msg = msg or ""
    return coroutine_yield("RETURN", msg, sz)
end

此函數的wiki文檔:

迴應一個消息可以使用 skynet.ret(message, size) 。它會將 message size 對應的消息附上當前消息的 session ,以及 skynet.PTYPE_RESPONSE 這個類別,發送給當前消息的來源 source 。由於某些歷史原因(早期的 skynet 默認消息類別是文本,而沒有經過特殊編碼),這個 API 被設計成傳遞一個 C 指針和長度,而不是經過當前消息的 pack 函數打包。或者你也可以省略 size 而傳入一個字符串。

由於 skynet 中最常用的消息類別是 lua ,這種消息是經過 skynet.pack 打包的,所以慣用法是 skynet.ret(skynet.pack(...)) 。btw,skynet.pack(…) 返回一個 lightuserdata 和一個長度,符合 skynet.ret 的參數需求;與之對應的是 skynet.unpack(message, size) 它可以把一個 C 指針加長度的消息解碼成一組 Lua 對象。

skynet.ret 在同一個消息處理的 coroutine 中只可以被調用一次,多次調用會觸發異常。有時候,你需要掛起一個請求,等將來時機滿足,再回應它。而回應的時候已經在別的 coroutine 中了。針對這種情況,你可以調用 skynet.response(skynet.pack) 獲得一個閉包,以後調用這個閉包即可把迴應消息發回。這裏的參數 skynet.pack 是可選的,你可以傳入其它打包函數,默認即是 skynet.pack 。

關鍵代碼

function skynet.ret(msg, sz)
    msg = msg or ""
    return coroutine_yield("RETURN", msg, sz)
end

-- 上面yield "RETURN" 後,會走到這裏來
function suspend(co, result, command, param, size)
    ...
    if command == "RETURN" then
        local co_session = session_coroutine_id[co]
        local co_address = session_coroutine_address[co]
        if param == nil or session_response[co] then
            error(debug.traceback(co))
        end
        session_response[co] = true
        local ret
        if not dead_service[co_address] then
            ret = c.send(co_address, skynet.PTYPE_RESPONSE, co_session, param, size) ~= nil
            if not ret then
                -- If the package is too large, returns nil. so we should report error back
                c.send(co_address, skynet.PTYPE_ERROR, co_session, "")
            end
        elseif size ~= nil then
            c.trash(param, size)
            ret = false
        end
        return suspend(co, coroutine.resume(co, ret))
    end
    ...
end

可以看出,這個函數在調用yield後,suspend函數會馬上調用resume喚醒它,所以此函數是非阻塞 API 。


使用心得:
一般的使用習慣是把skynet.ret作爲回調函數的最後一句,在這句執行完後,整個回調函數就結束了,其協程也將被回收。

類似這樣:

skynet.start(function()
    skynet.dispatch("lua", function(session, address, cmd, ...)
        local f = command[string.upper(cmd)]
        if f then
            skynet.ret(skynet.pack(f(...)))
        else
            error(string.format("Unknown command %s", tostring(cmd)))
        end
    end)
    skynet.register "SIMPLEDB"
end)

skynet.response

function skynet.response(pack)
    pack = pack or skynet.pack
    return coroutine_yield("RESPONSE", pack)
end

這個函數的喚醒方式同ret一樣,也是在suspend函數內重新喚醒。
同樣也是非阻塞API。

關於此函數更詳細的分析,請參考這篇文章:skynet.response分析

skynet.sleep

參考。。

skynet.wait

參考。。

skynet.exit

todo

function skynet.exit()
    fork_queue = {} -- no fork coroutine can be execute after skynet.exit
    skynet.send(".launcher","lua","REMOVE",skynet.self(), false)
    -- report the sources that call me
    for co, session in pairs(session_coroutine_id) do
        local address = session_coroutine_address[co]
        if session~=0 and address then
            c.redirect(address, 0, skynet.PTYPE_ERROR, session, "")
        end
    end
    for resp in pairs(unresponse) do
        resp(false)
    end
    -- report the sources I call but haven't return
    local tmp = {}
    for session, address in pairs(watching_session) do
        tmp[address] = true
    end
    for address in pairs(tmp) do
        c.redirect(address, 0, skynet.PTYPE_ERROR, 0, "")
    end
    c.command("EXIT")
    -- quit service
    coroutine_yield "QUIT"
end
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章