spdlog源码学习(下)

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.hinclude\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_logpost_flush函数给线程池发消息,告诉线程池有新的任务要做。同时,这两个函数也是重写了logger的同名函数,可以统一接口

backend_sink_it_backend_flush_这两个是由线程池进行调用的,是真正执行任务的函数。

由此可见,要想弄懂异步logger,有必要看一下线程池的实现。

7.2. thread_pool

相关文件在include\spdlog\details\thread_pool.hinclude\spdlog\details\thread_pool-inl.h

首先看到的是async_msg_type枚举和async_msg类,看名字就和异步logger的消息相关。

async_msg_type是给线程池的消息的类型。线程池根据消息类型进行相应操作。每个async_msg都有其对应的async_msg_typeasync_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_logpost_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函数干了下面的事:

  1. 获取registry;
  2. 给registry构造线程池;
  3. 构造sink和async_logger;
  4. 将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就结束啦!主要是如下步骤:

  1. (可选)初始化线程池参数。线程池构造函数中执行主循环,尝试从队列中取消息;

  2. 调用不同async_overflow_policyasync_factory_impl下的create函数,构造sink和logger,并注册logger。线程池构造函数中执行主循环,尝试从队列中取消息;

  3. 需要日志输出时,调用logger的sink_it_函数,在该函数中调用logger持有的线程池对象的post_log函数;

  4. post_log中构造async_msgasync_msg中持有logger对象),而后调用post_async_msg_向队列中插入提交消息;

  5. 提交消息后,队列会唤醒线程池主循环中因为队空而阻塞的线程,让消息被线程池取出;

  6. 线程池根据消息类型,通过消息持有的logger对象,调用backend_xxx函数;

  7. backend_xxx函数调用所有sink的对应操作,完成对一条消息的处理;

  8. 线程池析构时,向队列发送等同于线程数量的terminate消息,终止所有线程,等待线程退出。

八、异常

对于异常的捕获是在logger当中,spdlog用的是宏定义SPDLOG_TRYSPDLOG_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

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值