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