« 有时候还真要信命 | 返回首页 | 数学运算的实时编译及 Lua 中的一点奇技淫巧 »

线程安全的 log 回调函数

最近在做 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 的线程个数固定且有限。

Comments

丧失了高并发时的日志有序性? 比如同一个线程连续输出两次日志,有概率输出到不同buffer,而导致输出到文件时乱序? 不如老老实实来个多写单读缓冲区。实现一个这个缓冲区,用处很广。6年前实现的这样的缓冲区,现在到处都用得到
学习了,原来写 log 的时候还会出现这种情况...
云风哥,buffer的对应使用场景和数量级别能透露一下吗? log丢失了还是很不利于的案例处理,如果资源还有剩余,可以判断到buffer装满写入文件。 还有就是像楼上说的,在一些特殊的地方使用全局的变量counter来标明控制的计数也利于分析,我是经常这样干,只要不在关键榨取性能的地方都搞不死逻辑。
buffer个数是多少呢?
云风看看这个无锁队列合适不?代码足够简单(参考UE4里面的无锁队列搞的)。 https://github.com/OttoX/cetus/blob/master/container/mpsc_queue.h
这里次序不那么重要,错了一点也没关系。合并多个 buffer 的时候标出 buffer 的位置,可以推断出来即可。如果需要保证,加个原子递增的序号也比较容易。
每条log都可能写到任何buffer, 如何确保同一线程的日志顺序? 每条日志都加序号?

Post a comment

非这个主题相关的留言请到:留言本