spdlog源码学习(下)
写在前面
我使用的spdlog版本是1.10.0,spdlog的最新版可以在https://github.com/gabime/spdlog找到。本文相关的代码和笔记可以在我的github仓库: https://github.com/KuiH/read-spdlog1.10.0找到。仓库中的代码有我自己加的注释。
由于我本人也是出于学习的目的想要读读这份源码,并且我还很菜,所以下面的内容难免有出错的地方,欢迎大家在评论区指出错误,或者私信我也可以!
简单的目录
七、异步logger
异步logger可以使日志在后台输出。比如在代码中某一行要输出1000条日志,如果用普通logger的话,就要等1000条都输出完后才能继续运行后面的代码;如果用异步logger,就可以一边运行后面的代码,一边让日志输出。
7.1. async_logger
相关文件位于include\spdlog\async_logger.h
和include\spdlog\async_logger-inl.h
。
在.h中首先看到的就是一个枚举async_overflow_policy
,看名字是应对某种溢出的方式。这个枚举的作用我们在7.2节说。我们这里主要看看async_logger
类。该类继承自logger
。
成员属性:
std::weak_ptr<details::thread_pool> thread_pool_;
async_overflow_policy overflow_policy_;
就一个线程池和一个溢出应对政策,没啥好说。但是这里的thread_pool_
用的是std::weak_ptr
而不是shared_ptr
,可能使为了防止循环引用(即thread_pool_
中也用了async_logger指针)导致智能指针无法析构。
部分成员函数:
void sink_it_(const details::log_msg &msg) override;
void flush_() override;
void backend_sink_it_(const details::log_msg &incoming_log_msg);
void backend_flush_();
根据自带的注释,sink_it_
和flush_
分别通过thread_pool_
的post_log
和post_flush
函数给线程池发消息,告诉线程池有新的任务要做。同时,这两个函数也是重写了logger
的同名函数,可以统一接口。
backend_sink_it_
和backend_flush_
这两个是由线程池进行调用的,是真正执行任务的函数。
由此可见,要想弄懂异步logger,有必要看一下线程池的实现。
7.2. thread_pool
相关文件在include\spdlog\details\thread_pool.h
和include\spdlog\details\thread_pool-inl.h
。
首先看到的是async_msg_type
枚举和async_msg
类,看名字就和异步logger的消息相关。
async_msg_type
是给线程池的消息的类型。线程池根据消息类型进行相应操作。每个async_msg
都有其对应的async_msg_type
。async_msg_type
如下:
enum class async_msg_type
{
log,
flush,
terminate
};
log
:该消息是日志输出flush
:该消息是需要刷新terminate
:该消息要终止取出它的线程
至于async_msg
类,都是些构造函数,这里就不介绍。值得注意的是,该类保存了异步logger的指针,而普通的log_msg只是记录logger的名字。并且该类是只能move不能copy的。
下面看看thread_pool
类。
成员变量就两个:
q_type q_; // 消息队列,是一个多生产者多消费者的阻塞队列
std::vector<std::thread> threads_; // 所有线程
成员函数:
- 构造函数
构造函数中初始化消息队列大小、最大线程数、每个线程开始和结束的动作,并调用成员函数worker_loop_
开启线程池。
void worker_loop_();
一个空的while循环,以process_next_msg_
的返回值为循环条件。
bool process_next_msg_();
处理队列中的下一条信息。如果线程池没有收到terminate消息则返回true。从队列中取出一条消息,并根据消息的类型选择合适的处理函数。
void post_log(async_logger_ptr &&worker_ptr, const details::log_msg &msg, async_overflow_policy overflow_policy);
构造async_msg
对象后,向线程池的消息队列中添加一条类型为log
的消息
void post_flush(async_logger_ptr &&worker_ptr, async_overflow_policy overflow_policy);
向线程池的消息队列中添加一条类型为flush
的消息
void post_async_msg_(async_msg &&new_msg, async_overflow_policy overflow_policy);
post_log
和post_flush
最终都会调用到这个函数,该函数根据overflow_policy
选择消息入队时处理队满的操作。现在可以介绍overflow_policy
的枚举值了。block
表示当队列满了时,新消息一直阻塞,直到有足够空间;overrun_oldest
表示当队列满了时,新消息覆盖掉最老的消息。
~thread_pool();
析构函数中构造数量等于线程数的terminate
消息,并让线程join,等待所有线程终止。
7.3. mpmc_blocking_queue
线程池中的消息都会存到这个队列里,我对这个多生产者多消费者的队列也比较感兴趣,所以来看看。主要是看它是怎么样实现互斥和同步的。相关内容在include\spdlog\details\mpmc_blocking_q.h
。
成员变量:
std::mutex queue_mutex_; // 互斥量
std::condition_variable push_cv_; // 同步量
std::condition_variable pop_cv_;
spdlog::details::circular_q<T> q_; // 循环队列
成员函数:
void enqueue(T &&item)
item入队,队满则阻塞。上面的post_async_msg_
函数,如果传入策略为block
,则会调用这个函数。详细的解释看代码中的注释。
void enqueue_nowait(T &&item)
item入队,队满则覆盖。上面的post_async_msg_
函数,如果传入策略为overrun_oldest
,则会调用这个函数。
bool dequeue_for(T &popped_item, std::chrono::milliseconds wait_duration)
出队。如果队空则等待wait_duration
时间后再尝试出队一次。如果成功出队,则返回true,否则返回false。
7.4. async_factory_impl
这是异步日志的最后一个小节了!从example中可以发现,使用异步logger是要用到async_factory
,所以在了解完前面的实现细节后,我们来看看这个async_factory
是咋用的。相关文件在include\spdlog\async.h
主要类为async_factory_impl
,接受一个模板参数async_overflow_policy
:
template<async_overflow_policy OverflowPolicy = async_overflow_policy::block>
struct async_factory_impl
这个类就一个成员函数create
,与synchronous_factory
接口保持一致。create
函数干了下面的事:
- 获取registry;
- 给registry构造线程池;
- 构造sink和async_logger;
- 将async_logger注册进registry。
对于不同的async_overflow_policy
,构建了2个不同的类型:
using async_factory = async_factory_impl<async_overflow_policy::block>;
using async_factory_nonblock = async_factory_impl<async_overflow_policy::overrun_oldest>;
而后,在include\spdlog\async.h
中提供了构造不同policy的async_logger的全局函数:
template<typename Sink, typename... SinkArgs>
inline std::shared_ptr<spdlog::logger> create_async(std::string logger_name, SinkArgs &&... sink_args)
{
return async_factory::create<Sink>(std::move(logger_name), std::forward<SinkArgs>(sink_args)...);
}
template<typename Sink, typename... SinkArgs>
inline std::shared_ptr<spdlog::logger> create_async_nb(std::string logger_name, SinkArgs &&... sink_args)
{
return async_factory_nonblock::create<Sink>(std::move(logger_name), std::forward<SinkArgs>(sink_args)...);
}
如果想要自己定义线程池的最大线程数和队列的最大size,可以在构造async_logger前调用include\spdlog\async.h
下的全局函数init_thread_pool
,该函数会向registry中设置线程池。
至此,异步logger就结束啦!主要是如下步骤:
-
(可选)初始化线程池参数。线程池构造函数中执行主循环,尝试从队列中取消息;
-
调用不同
async_overflow_policy
的async_factory_impl
下的create
函数,构造sink和logger,并注册logger。线程池构造函数中执行主循环,尝试从队列中取消息; -
需要日志输出时,调用logger的
sink_it_
函数,在该函数中调用logger持有的线程池对象的post_log
函数; -
在
post_log
中构造async_msg
(async_msg
中持有logger对象),而后调用post_async_msg_
向队列中插入提交消息; -
提交消息后,队列会唤醒线程池主循环中因为队空而阻塞的线程,让消息被线程池取出;
-
线程池根据消息类型,通过消息持有的logger对象,调用
backend_xxx
函数; -
backend_xxx
函数调用所有sink的对应操作,完成对一条消息的处理; -
线程池析构时,向队列发送等同于线程数量的terminate消息,终止所有线程,等待线程退出。
八、异常
对于异常的捕获是在logger当中,spdlog用的是宏定义SPDLOG_TRY
和SPDLOG_LOGGER_CATCH(location)
,catch中调用logger的err_handler_
函数来处理异常。
我们在这一个部分主要是看看spdlog是怎么抛出异常的。相关文件在include\spdlog\common.h
以及include\spdlog\common-inl.h
。
要想让异常比较灵活,就得自己定义异常类。这里是spdlog_ex
类,继承了std::exception
。这个类比较简单,唯一成员就是std::string msg_
,表示异常信息。类中重载了const char *what() const
函数,该函数返回msg_
的const char *版本。
提供了2个抛出异常的全局函数throw_spdlog_ex
,负责构造spdlog_ex
并throw出来。需要抛出异常时调用这个函数就好。例如:
throw_spdlog_ex("Failed re opening file - was not opened before")
或
throw_spdlog_ex("Failed opening file " + os::filename_to_str(filename_) + " for writing", errno)
其中,errno
为std的宏。程序开始执行的时候,errno
会被置 0,当一个系统调用出错时,errno
会被置上一个非 0 的值。不同的errno
对应不同错误,可以通过strerror(errno)
来得到具体的错误。
九、尾声
spdlog的阅读到此结束。下面给出我个人在初步类图基础上继续完善的类图:
完结撒花!
上半部分博客链接:
https://blog.csdn.net/weixin_51063895/article/details/132392476