skynet 某些庫導致 attempt to yield across a C-call boundary 錯誤的問題

問題描述

在使用 skynet 提供的一些庫的時候,報 attempt to yield across a C-call boundary 的錯誤。

常見的有以下這些:
* datasheet
* multicast
* cluster
* sharedata
* …

比如我們在某個 lua 文件內 require(“skynet.datasheet”), 在運行到這個文件時,會報上面的錯誤。

問題原因

在說原因之前,可以先看下 issue 裏的討論。

一個關於 yield across a C-call boundary 的問題。

雲風:

導致 yield across a C-call boundary 的原因

C (skynet framework)->lua (skynet service) -> C -> lua

最後這個 lua 裏如果調用了 yield 就會產生。

這裏 require 就是一個 C 函數.

skynet.init 就是爲了避免你在 require 階段運行阻塞代碼, https://github.com/cloudwu/skynet/blob/master/lualib/skynet.lua#L588-L600
把它推遲到 skynet.start 再運行。

除非你別的地方破壞了這個行爲,讓 https://github.com/cloudwu/skynet/blob/master/lualib/skynet.lua#L602 不小心運行了。


issue 裏說的已經比較清楚了,我再囉嗦一下,其實就是在C函數內調用了 coroutine.yield(),一般我們遇到這個問題,十有八九都是 require 導致的。

這裏以 datasheet 爲例,說下爲什麼會在 require 時調用 yield。

datasheet/init.lua 的開始處,有這麼一段代碼:

skynet.init(function()
    datasheet_svr = service.query "datasheet"
end)

先看下 skynet.init 是幹嘛的:

如果你想在 skynet.start 註冊的函數之前做點什麼,可以調用 skynet.init(function() … end) 。這通常用於 lua 庫的編寫。你需要編寫的服務引用你的庫的時候,事先調用一些 skynet 阻塞 API ,就可以用 skynet.init 把這些工作註冊在 start 之前。

再看下其實現:

function skynet.init(f, name)
    assert(type(f) == "function")
    if init_func == nil then    --重點關注下這裏
        f()
    else
        table.insert(init_func, f)
        if name then
            assert(type(name) == "string")
            assert(init_func[name] == nil)
            init_func[name] = f
        end
    end
end

可以看到,在 skynet.init 時,如果 init_func 爲 nil,就會直接執行 f 這個函數,而不是把它放到 init_func 隊列裏。

而 skynet.init 這個函數,是暴露在最外層的,也就是說這個函數在 require 時就會被調用,這時如果 init_func 爲 nil,就會直接執行 f(), 而 f() 內有 yield。

那爲什麼 init_func 會爲 nil 呢?

通過看代碼,我們知道 init_func 是一個 table, 每次調用 skynet.init 時,都會把傳進來的函數插入到這個表內,然後在 skynet.start 時,在真正調用 start 指定的函數之前,先把 init_func 內的函數全部執行一遍,然後把 init_func 置爲 nil 。

代碼在這裏:

-- 此函數會在 skynet.start 指定的函數調用前調用
local function init_all()
    print(debug.traceback())
    local funcs = init_func
    init_func = nil -- init_func被置爲nil
    if funcs then
        for _,f in ipairs(funcs) do
            f()
        end
    end
end

也就是說,只要調用了 skynet.start, 之後再調用 skynet.init 就是直接執行其傳進去的函數了。

這樣做的設計思路應該是:服務已經啓動了,init func 的執行時機已過,所以就直接執行吧。

這樣設計本來也無可厚非,但問題就在於,skynet 裏有很多模塊是在 skynet.init 裏去做一些查詢地址之類的操作,這些操作會調用 skynet.call, 大家都知道,這個函數會 yield。這就相當於把 skynet.call 直接暴露在最外面,require 時就執行,那肯定會報錯嘛。

解決方法

很簡單,既然在 skynet.start 之後不能調用 skynet.init 了,那我們就在 skynet.start 之前調用,也就是把要用到的這類模塊(含有這種代碼:skynet.init(function() skynet.call(…)end)),統統在服務啓動前 require 一遍。

如下所示:

-- 假設這個服務用到了這三個模塊
require("skynet.datasheet")
require("skynet.cluster")
require("skynet.multicast")

function init()

end

function exit()

end

最後附上錯誤LOG:

[:00000022] lua call [20 to :22 : 9 msgsz = 29] error : 3rd/skynet/lualib/skynet.lua:534: 3rd/skynet/lualib/skynet.lua:156: 3rd/skynet/service/snaxd.lua:46: attempt to yield across a C-call boundary
stack traceback:
    [C]: in function 'skynet.profile.yield'
    3rd/skynet/lualib/skynet.lua:388: in upvalue 'yield_call'
    3rd/skynet/lualib/skynet.lua:402: in function 'skynet.call'
    3rd/skynet/lualib/skynet.lua:545: in function 'skynet.uniqueservice'
    3rd/skynet/lualib/skynet/service.lua:8: in upvalue 'get_provider'
    3rd/skynet/lualib/skynet/service.lua:39: in function 'skynet.service.query'
    3rd/skynet/lualib/skynet/datasheet/init.lua:8: in local 'f'
    3rd/skynet/lualib/skynet.lua:600: in function 'skynet.init' 這一行告訴我們 skynet.init 內直接執行了函數,而不是插入初始隊列,那肯定是skynet.start執行過了
    3rd/skynet/lualib/skynet/datasheet/init.lua:7: in main chunk
    [C]: in function 'require' 這裏指出了是在C裏的 require 函數出錯的
    server/game_server/common/localdata/localDataLogic.lua:15: in main chunk
    [C]: in function 'require'
    common/globalfunc.lua:73: in metamethod '__index'

END

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章