skynet 某些库导致 attempt to yield across a C-call boundary 错误的问题

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/gneveek/article/details/78962949

问题描述

在使用 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 再运行。

扫描二维码关注公众号,回复: 3287512 查看本文章

除非你别的地方破坏了这个行为,让 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

猜你喜欢

转载自blog.csdn.net/gneveek/article/details/78962949