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

你可能感兴趣的:(skynet)