assert 用于断言某条件必须成立,否者就会触发断言失败,比如在 C 中进程会退出并打印出行号,而 Lua 中打印出 traceback 。
有时候会听人说要学会使用 assert 。当时并不理解,因为 assert 就是断言某条件必须成立,很简单啊,怎么还特别强调要学会呢。直到后来看见关于 skynet 的多线程读写的 后,才体会到 assert 的用法,就是使用 assert 来限定代码的边界,掌控代码的执行,如果 assert 断言失败,可以很明确代码问题所在。skynet 的 产生原因如下。在 中,有用户无法编译通过,于是云大在之后做了些,将 uint64_t sending
修改为 uint32_t sending
来确保在某些平台能编译通过。但是解决 后,没有想到触发了 assert 断言失败,产生了 。这里就想简单介绍一下,在解决 时增加的 assert 代码。
先说修复 编译不通过前,多线程写 socket 的实现。云大在中上传了 skynet 支持多线程写入代码。一开始定义就是 uint64_sending
被分成两部分,高 32 位是分配的 id,低 32 位表示 socket 线程中此 id 正在发送的包数量,若低 32 位为 0,则表示此时 socket 线程中还未有需要发送的数据。所以 service 需要发送数据时,在 worker 线程调用 socket_server_send
函数,此时若能直接 write 则直接调用 write 写入数据,若不能直接 write 则会增加 sending
计数,通过 socket 线程发送,发送完毕后减少 sending
计数。
在多线程写 socket 实现提交后,产生了 存在某些平台上编译不通过,于是云大改成了 uint32_t sending
。这里有一个技巧是 skynet 分配的 socket id 是递增的,具体实现见 reserve_id
函数,而且 id 被 HASH_ID
的 hash 处理也只是求余操作,那么如果 slot 1 被占用,就不会存在第二个被使用的 HASH_ID(id)
为 1 的 socket id 。所以通过 ID_TAG16
取出 slot 进而判断 socket_server.slot
是否被使用了,于是 sending
的高 16 位用于标识 id 对应的 slot 而低 16 位用于计数。这里就存在了一个前提,计数的最大值是 65535 。所以云大在中的第 571 行加了 assert 确保计数一超过 65535 就会断言失败。真的有情景触发了断言失败,然后就产生了前文提到的的 问题。由于 bug 触发了 assert 断言失败,于是可以目的很明确的修复这个问题。话说回来,多线程写 socket 还真是挺复杂的。
其实 Lua 的代码中也在很多地方用了 assert 。不过 Lua 代码比较复杂,这里再举个例子,比如 skynet 中的 skynet.queue 模块,其中有用 assert 。位于 skynet/lualib/skynet/queue.lua 文件。部分代码如下。
function skynet.queue() local current_thread local ref = 0 local thread_queue = {} return function(f, ...) local thread = coroutine.running() if current_thread and current_thread ~= thread then table.insert(thread_queue, thread) skynet.wait() assert(ref == 0) -- current_thread == thread end current_thread = thread ref = ref + 1 return xpcall_ret(xpcall(f, traceback, ...)) endend
其中 assert(ref == 0)
确保之前的 current_thread
已经执行完毕。注意,当在同一个 coroutine 中嵌套调用时 ref
是可能大于 1 的。如。
local lock = skynet.queue()lock(function() print("one") lock(function() print("two") end)end)
总之 assert 真的是个好东西。