lua服务在执行回调函数的过程中,调用某些函数会挂起协程,比如skynet.call, skynet.ret, skynet.response等等,这些函数把协程挂起后,如何唤醒呢?
本文将对所有调用coroutine.yield
的API的唤醒做下分析。(比较拗口,找不到更好的表达方式了)
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流程
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。
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)
function skynet.response(pack)
pack = pack or skynet.pack
return coroutine_yield("RESPONSE", pack)
end
这个函数的唤醒方式同ret一样,也是在suspend函数内重新唤醒。
同样也是非阻塞API。
关于此函数更详细的分析,请参考这篇文章:skynet.response分析
参考。。
参考。。
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