问题描述
在使用 skynet 提供的一些库的时候,报 attempt to yield across a C-call boundary
的错误。
常见的有以下这些:
* datasheet
* multicast
* cluster
* sharedata
* …
比如我们在某个 lua 文件内 require(“skynet.datasheet”), 在运行到这个文件时,会报上面的错误。
问题原因
在说原因之前,可以先看下 issue 里的讨论。
云风:
导致 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