前文

日志库spdlog(一) 源码面前,了无秘密 spdlog · Code Analysis 正如前文所说,本文将聚焦于spdlog在并发方面的工作,同时提点一下值得一提的技术。

线程安全

C++支持多线程编程,既然支持并发(并发或者并行是有区别的,但常规而言并发用的更多,读者明白就好),那spdlog库就天然存在重入数据竞争的问题。 比如,在registry层,存储一个name to logger的映射,此时如果并发写入,显然会有UB。 再比如,如果多个线程同时对一个logger写入日志,那此时如果没有保护,显然输出会乱掉。

spdlog为了支持并发,几乎在每一层都放了mutex来保护数据。

registry层

registry层主要有三个锁,如下:

1std::mutex logger_map_mutex_, flusher_mutex_;
2std::recursive_mutex tp_mutex_;

前两个锁结合代码都很好理解:前者用于保护name to logger结构,后者用于保护定时会写的结构(支持定时刷盘,多线程实现)。 在对logger执行操作时,此时不希望logger被更改,应当上锁,以达到快照功能:

1SPDLOG_INLINE void registry::set_level(level::level_enum log_level) {
2    // 如果要设置日志等级,此时就不允许其他线程修改logger
3    std::lock_guard<std::mutex> lock(logger_map_mutex_);
4    for (auto &l : loggers_) {
5        l.second->set_level(log_level);
6    }
7    global_log_level_ = log_level;
8}

最后一个则是用到了std::recursive_mutex,这是C++的可重入锁,也就是说可以被多个owner持有,内部存在计数,被锁上多少次就要被解锁多少次。 其作用暂且按下不表,等将异步时再说明。

logger层

logger负责整合sink,实际上的并发安全是由sink保证的。当然这里logger初始化的时候会有些风险,显然你不应该在两个线程内同时去初始化同一个logger

sink层

该层负责真正写入时的并发安全,但sink基类却没有锁,这就比较奇怪了,回过头我们再看看两种sink的实现:

 1// ansicolor_sink.h
 2template <typename ConsoleMutex>
 3class ansicolor_sink : public sink {
 4    ...
 5    using mutex_t = typename ConsoleMutex::mutex_t;
 6    mutex_t &mutex_;
 7    ...
 8}
 9
10using ansicolor_stdout_sink_mt = ansicolor_stdout_sink<details::console_mutex>;
11using ansicolor_stdout_sink_st = ansicolor_stdout_sink<details::console_nullmutex>;
12
13// basic_file_sink.h
14template <typename Mutex>
15class basic_file_sink final : public base_sink<Mutex> {
16};
17
18using basic_file_sink_mt = basic_file_sink<std::mutex>;
19using basic_file_sink_st = basic_file_sink<details::null_mutex>;
20
21// base_sink.h
22template <typename Mutex>
23class SPDLOG_API base_sink : public sink {
24    ...
25    Mutex mutex_;
26    ...
27};

可以看到,对于不同的sink,都会定义一个mutex,以此实现多线程下的写入安全。 在使用部分,则是在log、flush等多处使用,保证临界区写入安全。 对于一个sink,提供了_mt _st两类后缀类型实例,传入了正常的mutex锁以及只实现了lock、unlock两个空方法的结构,有兴趣可以看看。 这样对于任意一个类型的sink,不必两种类型都实现一遍,空方法编译器是会优化掉的。

小结

spdlog在并发上做的很严谨,正如上文所说,锁对性能也有一定影响,锁基本也只加在了需要的地方。 显然,针对单线程场景,我们使用single thread类型的sink,针对多线程场景,则使用multi thread类型的,便捷切换,直接改个mt即可,你大概率也不会有任何心智负担。

异步