线程安全的 log 回调函数

云风 2018-07-06 19:46

最近在做 3d engine 时发现,我们使用的渲染 api 库 bgfx 提供的 log 回调函数是需要自己保证线程安全的。也就是说 bgfx 有可能在不同线程(采用多线程渲染时)调用这个 log 回调函数。

如果回调函数仅仅只是把 log 串写入文件(例如标准输出),那么可以由 crt 本身来保证线程安全。但如果想自己处理 log ,例如把 log 串压回 lua 虚拟机,那么就必须自己负责线程安全的问题。

我们现在的做法是提供一个线程安全的 log buffer ,让 log 回调函数使用。然后在 lua 中每帧把 log buffer 中的数据复制回 lua vm 。

在实现 thread safe 的 log buffer 模块时,我想到了一个有趣的优化手段。

一般的 3d 程序里,线程并不多,无非是主业务线程和渲染线程。最简单的方法是用 TLS ,为每个线程创建一个 buffer ,这样大家就互不干扰。但我不太想在我的 bgfx lua 封装库中引入 TLS 这个特性,所以我采用了另外一个类似的方法。

使用无锁数据结构也是一个选择,但无锁数据结构实现往往非常实现错,做不到足够简单到明显没有 bug ,所以我也不想采用。

首先创建出多个 buffer 结构,每个都有独立的锁。写入 log 的时候,依次锁每个 buffer ,如果加锁失败就尝试下一个,直到最后一个再直接等待锁成功。由于线程数量有限,所以通常是不会到最坏情况的。log 锁住哪个 buffer 就写入哪个。

在读 buffer 的时候,同样以此尝试锁 buffer ,但是如果加锁失败,则忽略。由于我们的读 log 的操作一定处于 lua vm 所在的业务线程,和大部分业务线程上 bgfx api 产生的 log 处于同一个线程,所以同一线程上的 log buffer 是一定可以读出的。冲突的一般都是渲染线程上的 log buffer 。但我们每帧都会读一次,所以迟早都会读出。

和基于 CAS 的无锁算法一样,理论上存在饿死(永远读不到某个 buffer )的可能,但实际上并不会发生。而且,客户端 log 重要性不大,每个 log buffer 我直接使用了一块 64K 固定内存的 ring buffer 来实现,如果没有及时读走而 buffer 装满后就不再写入,同时也不锁 buffer ,这样读取方也避免了理论上的饿死可能(写入者不再加锁,读取方就一定能获取到锁)。

以上算法都是为了回避业务线程和渲染线程同时写 log 时导致的锁竞争问题。因为写 log 而破坏了并行性感觉不太值当。

而此算法可以成立,且能实现的足够简单,是建立在如下前提上:

log 在极端情况下允许丢失,所以采用固定大小的 ring buffer 简化 buffer 实现。

不同线程的 log 无需保证次序。

写 log 的线程个数固定且有限。

[返回] [原文链接]