本文主要是介绍spdlog日志库源码:输出通道sink,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!
概述
在 spdlog 日志库中,sinks 并不是一个单独的类,而是一系列类的集合,这些类以基类-派生类的形式组织,每一个 sink 派生类代表了一种输出日志消息的方式。输出目标可以是普通文件、标准输出 (stdout)、标准错误输出 (stderr)、系统日志 (syslog) 等等。其文件位于include/spdlog/sinks中
sink类
类sink是所有sinks系列类的基类,也是一个接口类,提供接口和共有数据,但不负责实例化。
/// 一个sink对象对应一个输出目标, 即文件, 负责将log消息写到指定目标上,
/// 可能是普通文件, syslog, 终端, 或者socket(tcp/udp), etc.
class SPDLOG_API sink
{
public:virtual ~sink() = default;// 接收log消息virtual void log(const details::log_msg &msg) = 0;// 冲刷log消息virtual void flush() = 0;// 设置模式virtual void set_pattern(const std::string &pattern) = 0;// 设置formatter(格式)virtual void set_formatter(std::unique_ptr<spdlog::formatter> sink_formatter) = 0;// 设置log等级阈值void set_level(level::level_enum log_level);// 获取log等级阈值level::level_enum level() const;// 判断是否应当写log消息,msg_level是log消息的log等级bool should_log(level::level_enum msg_level) const;
protected:// sink log level - default is alllevel_t level_{level::trace};
};
- log() 接收用户log消息并写入目标文件,通常是由logger类传入
- flush() 冲刷用户log消息,将缓存中数据尽快写入目标文件
- set_pattern() 由现有的模式标志,定制输出的log消息格式
- set_formatter() 实现自定义formatter,定制输出的log消息格式
sink有一个比较特殊的变量level_,是指sink的日志等级,相当于一个log等级阈值。只有当log消息本身log等级 >= level_时,才写log到目标。should_log函数中会判断是否写:
SPDLOG_INLINE bool spdlog::sinks::sink::should_log(spdlog::level::level_enum msg_level) const {return msg_level >= level_.load(std::memory_order_relaxed);
}
sink提供了level_的get、set方法。注意这里并没有直接对leve_使用"="进行赋值,而是使用了适用于内存布局的方法(松散的内存顺序)。
SPDLOG_INLINE void spdlog::sinks::sink::set_level(level::level_enum log_level) {level_.store(log_level, std::memory_order_relaxed);
}SPDLOG_INLINE spdlog::level::level_enum spdlog::sinks::sink::level() const {return static_cast<spdlog::level::level_enum>(level_.load(std::memory_order_relaxed));
}
sink子类
base_sink类模板
这是sink最核心的一个子类,是一个抽象类类模板,无法实例化,为其他更多的sink提供公共的标准接口。
template<typename Mutex>
class SPDLOG_API base_sink : public sink
{
public:base_sink();explicit base_sink(std::unique_ptr<spdlog::formatter> formatter);~base_sink() override = default;base_sink(const base_sink &) = delete;base_sink(base_sink &&) = delete;base_sink &operator=(const base_sink &) = delete;base_sink &operator=(base_sink &&) = delete;void log(const details::log_msg &msg) final; // 接收用户log消息void flush() final; // 冲刷用户log消息(到目标文件)void set_pattern(const std::string &pattern) final; // 用模式串设置模式, 使用默认的formatter=pattern_formattervoid set_formatter(std::unique_ptr<spdlog::formatter> sink_formatter) final; // 设置formatter指定格式, 支持自定义formatterprotected:// sink formatterstd::unique_ptr<spdlog::formatter> formatter_;Mutex mutex_; // 通常为互斥锁或空锁virtual void sink_it_(const details::log_msg &msg) = 0;virtual void flush_() = 0;virtual void set_pattern_(const std::string &pattern);virtual void set_formatter_(std::unique_ptr<spdlog::formatter> sink_formatter);
};
base_sink在基类sink基础上做了一些额外工作,主要是:
- 添加接受formatter为参数的构造器;
- 删除拷贝构造、移动构造函数;
- 删除拷贝赋值、移动赋值运算符;
- 将方法log、flush、set_pattern、set_formatter声明为final,禁止派生类重写,但又增添了virtual版本的protected方法sink_it_、flush_、set_pattern_、set_formatter_,这实际上是模板方法(设计模式)的应用;
- 提供默认的formatter(即pattern_formatter),或自定义的formatter支持;
- 以模板参数Mutex为锁类型,便于用同一套代码实现有锁、无锁两套方案;
例如,要创建一个有锁的文件 sink,可以使用 std::mutex 作为模板参数:
#include <spdlog/sinks/basic_file_sink.h>auto file_sink = std::make_shared<spdlog::sinks::basic_file_sink_mt>("logs.txt", true);
// basic_file_sink_mt 是 basic_file_sink<std::mutex> 的类型定义
要创建一个无锁的文件 sink,可以使用 null_mutex 作为模板参数:
#include <spdlog/sinks/basic_file_sink.h>
#include <spdlog/details/null_mutex.h>auto file_sink = std::make_shared<spdlog::sinks::basic_file_sink<spdlog::details::null_mutex>>("logs.txt", true);
这个spdlog::details::null_mutex是结构体,实现了lock和unlock,不过是空函数。
struct null_mutex {void lock() const {}void unlock() const {}
};
//在std::lock_guard<Mutex> lock(mutex_)中会调用这两个函数
base_sink的public接口是线程安全的,只在public接口加锁,并未在protected方法加锁。例如,base_sink::log()接收log消息:
template<typename Mutex>
void SPDLOG_INLINE spdlog::sinks::base_sink<Mutex>::log(const details::log_msg &msg)
{std::lock_guard<Mutex> lock(mutex_); // 获得锁,确保base_sink<Mutex>数据成员的线程安全sink_it_(msg); // sink_it_是纯虚函数,实际工作转发给sink_it_
}
log()把工作转发给了virtual函数sink_it_,实际调用的是子类的实现。例如,其中一个子类basic_file_sink::sink_it_将msg进行格式化(format)后转换为二进制数据,然后通过工具类file_helper的write()写入目标文件,其实现如下:
template<typename Mutex>
SPDLOG_INLINE void basic_file_sink<Mutex>::sink_it_(const details::log_msg &msg)
{memory_buf_t formatted; // 二进制缓存base_sink<Mutex>::formatter_->format(msg, formatted);file_helper_.write(formatted); // 将格式化后的二进制数据写入目标文件
}
除了构造器,有数据访问的public接口都加锁了,而非public接口并未加锁。
null_sink类模板
用户想用logger对象,就必须提供sink对象。但是如果用户不想做任何写文件操作,例如测试代码框架是否能跑通,此时可以使用null_sink,是一个空类,所有接口皆为空。
template <typename Mutex>
class null_sink : public base_sink<Mutex> {
protected:void sink_it_(const details::log_msg &) override {}void flush_() override {}
};using null_sink_mt = null_sink<details::null_mutex>;
using null_sink_st = null_sink<details::null_mutex>;
有了具体的null_sink类型(null_sink_mt/null_sink_st),可以用工厂方法装配出logger对象。
template <typename Factory = spdlog::synchronous_factory>
inline std::shared_ptr<logger> null_logger_mt(const std::string &logger_name) {auto null_logger = Factory::template create<sinks::null_sink_mt>(logger_name);null_logger->set_level(level::off);return null_logger;
}template <typename Factory = spdlog::synchronous_factory>
inline std::shared_ptr<logger> null_logger_st(const std::string &logger_name) {auto null_logger = Factory::template create<sinks::null_sink_st>(logger_name);null_logger->set_level(level::off);return null_logger;
}
basic_file_sink类模板
basic_file_sink是basic_sink的派生类(类模板),提供文件操作,写log消息到指定文件的基本操作。如果只是想拥有简单的写log消息到文件的功能,那么可使用该sink子类。
basic_file_sink会根据构造器提供的文件名来创建一个log文件,文件支持截断功能。文件截断指的是在文件写入操作时,当文件已经存在时是否清空其内容,从文件头开始写入新的数据。
template<typename Mutex>
class basic_file_sink final : public base_sink<Mutex>
{
public:explicit basic_file_sink(const filename_t &filename, bool truncate = false, const file_event_handlers &event_handlers = {});const filename_t &filename() const;protected:// 实现了2个base_sink声明的pure virtual函数void sink_it_(const details::log_msg &msg) override;void flush_() override;private:details::file_helper file_helper_; // 文件操作帮助类, 是一个工具类
};
私有变量file_helper是文件工具类,封装了一些基本文件操作,专门用于日志文件操作。
在构造函数中:filename 类型通过别名filename_t进行包装,本质上一个字符串(std::string),因为在Windows可能需要支持宽字符。
truncate 指定是否使用文件截断功能,在打开文件时决定,通常以write + append或truncate方式打开。
event_handlers 通过一个结构体file_event_handlers包装了文件操作前后的事件,用户可以通过这种回调函数机制,指定在对应文件事件发生时要进行的动作。支持4类文件事件:打开文件前(before_open)、打开文件后(after_open)、关闭文件前(before_close)、关闭文件后(after_close)。file_event_handlers定义:
struct file_event_handlers
{file_event_handlers(): before_open(nullptr), after_open(nullptr), before_close(nullptr), after_close(nullptr){}std::function<void(const filename_t &filename)> before_open;std::function<void(const filename_t &filename, std::FILE *file_stream)> after_open;std::function<void(const filename_t &filename, std::FILE *file_stream)> before_close;std::function<void(const filename_t &filename)> after_close;
};
basic_file_sink实现了2个基类base_sink声明的纯虚函数:sink_it_,flush_。
template<typename Mutex>
SPDLOG_INLINE void basic_file_sink<Mutex>::sink_it_(const details::log_msg &msg)
{memory_buf_t formatted;base_sink<Mutex>::formatter_->format(msg, formatted);file_helper_.write(formatted);
}template<typename Mutex>
SPDLOG_INLINE void basic_file_sink<Mutex>::flush_()
{file_helper_.flush();
}
本质就是利用基类的formatter_,对log消息msg进行格式化(format),转换为二进制数据存放到memory_buf_t缓存中,然后通过工具函数file_helper_.write写到指定文件中。
flush_则是直接调用工具函数file_helper_.flush冲刷缓存到文件。
daily_file_sink类模板
daily_file_sink类可以按照一定的时间间隔将日志写入到不同的文件中,通常用于按日期分割日志文件。例如,下面调用spdlog::daily_logger_mt的例子,就能每天都创建一个daily_logger对应的log文件:
#include "spdlog/sinks/daily_file_sink.h"
...
// 每天的14:55, 在logs目录下, 创建新的日志文件, 如"daily_logger_2022-11-03"
auto daily_logger = spdlog::daily_logger_mt("daily_logger", "logs/daily", 14, 55);
daily_logger_mt是提供给用户创建logger的接口,实际调用了同步工厂方法synchronous_factory::create,创建logger对象:
template<typename Factory = spdlog::synchronous_factory>
inline std::shared_ptr<logger> daily_logger_mt(const std::string &logger_name, const filename_t &filename, int hour = 0, int minute = 0,bool truncate = false, uint16_t max_files = 0, const file_event_handlers &event_handlers = {})
{// daily_file_sink_mt为工厂方法指定要装配的sink类型, 后面的函数参数用于构造sink对象// 工厂方法会自动将新建的sin对象装配给新建的logger对象, 并用shared_ptr包裹返回给调用者return Factory::template create<sinks::daily_file_sink_mt>(logger_name, filename, hour, minute, truncate, max_files, event_handlers);
}
dist_sink类模板
dist_sink基础自base_sink,是一个sink复用器,包含一组sinks,当log调用时,可分发给所有sink。
template<typename Mutex>
class dist_sink : public base_sink<Mutex>
{
public:dist_sink() = default;explicit dist_sink(std::vector<std::shared_ptr<sink>> sinks): sinks_(sinks){}// 因为对应类类底层文件资源, 因此禁止拷贝dist_sink(const dist_sink &) = delete;dist_sink &operator=(const dist_sink &) = delete;...protected:...std::vector<std::shared_ptr<sink>> sinks_;
};
// 便捷类型
using dist_sink_mt = dist_sink<std::mutex>; // 线程安全版本
using dist_sink_st = dist_sink<details::null_mutex>; // 非线程安全版本
sink_it_ 将log消息写到目标文件。dist_sink的实现则是将log消息转交给每个sink对象来处理。
flush_ 将log消息从缓存冲刷到目标文件。dist_sink的实现也是交给每个sink对象来处理。
protected:void sink_it_(const details::log_msg &msg) override{for (auto &sink : sinks_){if (sink->should_log(msg.level)){sink->log(msg);}}}void flush_() override{for (auto &sink : sinks_){sink->flush();}}
对sink数组进行增删改查,属于public接口,需要加锁以确保线程安全。
public:// 增void add_sink(std::shared_ptr<sink> sink){std::lock_guard<Mutex> lock(base_sink<Mutex>::mutex_);sinks_.push_back(sink);}// 删void remove_sink(std::shared_ptr<sink> sink){std::lock_guard<Mutex> lock(base_sink<Mutex>::mutex_);sinks_.erase(std::remove(sinks_.begin(), sinks_.end(), sink), sinks_.end());}// 改void set_sinks(std::vector<std::shared_ptr<sink>> sinks){std::lock_guard<Mutex> lock(base_sink<Mutex>::mutex_);sinks_ = std::move(sinks);}// 查std::vector<std::shared_ptr<sink>> &sinks(){return sinks_;}
dup_filter_sink 类模板
dup_filter_sink 用于过滤一定时间内相同的log消息,只会写一条,不会都写到log文件。
例如,下面这段代码利用dup_filter_sink过滤相同的log消息。
template<typename Mutex>
class dup_filter_sink : public dist_sink<Mutex>
{
public:template<class Rep, class Period>explicit dup_filter_sink(std::chrono::duration<Rep, Period> max_skip_duration): max_skip_duration_{max_skip_duration}{}
protected:std::chrono::microseconds max_skip_duration_; // 过滤时间,单位:微秒log_clock::time_point last_msg_time_; // 上一次log消息时间点std::string last_msg_payload_; // log消息载荷,即用户写的文本内容size_t skip_counter_ = 0; // 过滤次数void sink_it_(const details::log_msg &msg) override; // 父类dist_sink定义的virtual函数...
};
using dup_filter_sink_mt = dup_filter_sink<std::mutex>;
using dup_filter_sink_st = dup_filter_sink<details::null_mutex>;
sink_it_ 是向目标文件写log消息。dup_filter_sink的做法是,先判断与规定时间内的上一次log消息是否相同,如果相同就过滤掉;如果就先写之前的过滤信息,然后。
过滤重复log消息,并不是悄无声息的,而是会写一个"Skipped n duplicate messages…"的提示信息。
protected:void sink_it_(const details::log_msg &msg) override{bool filtered = filter_(msg); // false表示应该过滤掉, true表示不应该if (!filtered){skip_counter_ += 1;return;}// 过滤了重复log消息, 但应产生对应的过滤信息// log the "skipped.." messageif (skip_counter_ > 0){char buf[64];auto msg_size = ::snprintf(buf, sizeof(buf), "Skipped %u duplicate messages..", static_cast<unsigned>(skip_counter_));if (msg_size > 0 && static_cast<size_t>(msg_size) < sizeof(buf)){// 调用父类sink_it_ 将log消息写入sink对象对应的目标文件, 因为是virtual函数, 所以需要显式调用details::log_msg skipped_msg{msg.logger_name, level::info, string_view_t{buf, static_cast<size_t>(msg_size)}};dist_sink<Mutex>::sink_it_(skipped_msg);}}// 通过父类sink_it_ 将log消息写入sink对象// log current messagedist_sink<Mutex>::sink_it_(msg);// 更新上一次消息状态last_msg_time_ = msg.time;skip_counter_ = 0;last_msg_payload_.assign(msg.payload.data(), msg.payload.data() + msg.payload.size());}
ringbuffer_sink类模板
通常,sink的目标是一个文件,而ringbuffer_sink的目标是一个环形缓冲区,即内存。如果想把log消息写到内存中缓存起来,那么可以使用ringbuffer_sink。
template<typename Mutex>
class ringbuffer_sink final : public base_sink<Mutex>
{
public:// 构造者指定环形缓冲区大小explicit ringbuffer_sink(size_t n_items): q_{n_items}{}std::vector<details::log_msg_buffer> last_raw(size_t lim = 0);std::vector<std::string> last_formatted(size_t lim = 0);...
private:details::circular_q<details::log_msg_buffer> q_; // sink的目标, 即一个环形缓冲区
};using ringbuffer_sink_mt = ringbuffer_sink<std::mutex>;
using ringbuffer_sink_st = ringbuffer_sink<details::null_mutex>;
sink_it_ 实现是简单的将log消息插入到环形缓冲区末尾,flush_ 则实现为一个空函数,因为没有内容需要写到文件。
protected:void sink_it_(const details::log_msg &msg) override{q_.push_back(details::log_msg_buffer{msg}); // 调用的是vector<>::push_back(log_msg_buffer &&)版本}void flush_() override {}
syslog_sink类模板
其他sink将log消息写到目标文件或内存,而syslog_sink则将log消息交给一个系统服务进程syslog(POSIX,Windows不支持),进而写到系统日志文件(由syslog完成)。syslog_sink采用RAII方式管理syslog资源,构造即调用openlog打开syslog,析构即调用closelog关闭syslog。
template<typename Mutex>
class syslog_sink : public base_sink<Mutex>
{
public:syslog_sink(std::string ident, int syslog_option, int syslog_facility, bool enable_formatting): enable_formatting_{enable_formatting}, syslog_levels_{{/* spdlog::level::trace */ LOG_DEBUG,/* spdlog::level::debug */ LOG_DEBUG,/* spdlog::level::info */ LOG_INFO,/* spdlog::level::warn */ LOG_WARNING,/* spdlog::level::err */ LOG_ERR,/* spdlog::level::critical */ LOG_CRIT,/* spdlog::level::off */ LOG_INFO}}, ident_{std::move(ident)}{// set ident to be program name if empty::openlog(ident_.empty() ? nullptr : ident_.c_str(), syslog_option, syslog_facility);}~syslog_sink() override{::closelog();}// 因为对应了底层系统资源, 因此禁用拷贝syslog_sink(const syslog_sink &) = delete;syslog_sink &operator=(const syslog_sink &) = delete;protected:void sink_it_(const details::log_msg &msg) override;void flush_() override;bool enable_formatting_ = false;private:using levels_array = std::array<int, 7>;levels_array syslog_levels_; // syslog日志等级数组const std::string ident_;int syslog_prio_from_level(const details::log_msg &msg) const;
};// 便捷类型
using syslog_sink_mt = syslog_sink<std::mutex>; // 线程安全版本
using syslog_sink_st = syslog_sink<details::null_mutex>; // 非现线程安全版本
sink_it_ 实现为对log消息格式化(根据需要),然后将内容通过syslog()接口转交给syslog服务。
flush_ 实现为空,因为不涉及本进程下的文件操作。
protected:void sink_it_(const details::log_msg &msg) override{string_view_t payload;memory_buf_t formatted;if (enable_formatting_){base_sink<Mutex>::formatter_->format(msg, formatted);payload = string_view_t(formatted.data(), formatted.size()); // 格式化log消息}else{payload = msg.payload; // 直接赋值为原始的用户消息(而非格式化的log消息)}size_t length = payload.size();// limit to max intif (length > static_cast<size_t>(std::numeric_limits<int>::max())){length = static_cast<size_t>(std::numeric_limits<int>::max());}// 将log消息转交给syslog,注意需要将日志等级进行转换::syslog(syslog_prio_from_level(msg), "%.*s", static_cast<int>(length), payload.data());}void flush_() override {}
用户写log消息时,使用的是spdlog日志等级,而syslog需要的是自身的日志等级,因此需要转换。syslog_prio_from_level负责将日志等级从spdlog日志等级转换为syslog日志等级。两种日志等级对应关系,在syslog_levels_构造的注释中,已经详细说明:
int syslog_prio_from_level(const details::log_msg &msg) const
{return syslog_levels_.at(static_cast<levels_array::size_type>(msg.level));
}
stdout_sink_base类模板
stdout_sink_base 是一个用于向控制台(标准输出或标准错误)输出日志消息的派生类模板。它继承自 sink 类,而不是 base_sink 类,并且专门处理向控制台输出日志的任务。
template<typename ConsoleMutex>
class stdout_sink_base : public sink
{
public:using mutex_t = typename ConsoleMutex::mutex_t;explicit stdout_sink_base(FILE *file);~stdout_sink_base() override = default;// 禁用拷贝和移动操作stdout_sink_base(const stdout_sink_base &other) = delete;stdout_sink_base(stdout_sink_base &&other) = delete;stdout_sink_base &operator=(const stdout_sink_base &other) = delete;stdout_sink_base &operator=(stdout_sink_base &&other) = delete;// 重写 sink 类的纯虚函数void log(const details::log_msg &msg) override;void flush() override;void set_pattern(const std::string &pattern) override;void set_formatter(std::unique_ptr<spdlog::formatter> sink_formatter) override;protected:mutex_t &mutex_; // 引用类型的互斥锁FILE *file_;std::unique_ptr<spdlog::formatter> formatter_;#ifdef _WIN32HANDLE handle_;
#endif
};
对于非 Windows 平台,需要传入 stdout 或 stderr 作为控制台文件指针,而在 Windows 平台,需要获取文件句柄进行文件 IO 操作。
template<typename ConsoleMutex>
SPDLOG_INLINE stdout_sink_base<ConsoleMutex>::stdout_sink_base(FILE *file): mutex_(ConsoleMutex::mutex()) // 从模板参数 ConsoleMutex 中获取 mutex 对象,绑定到引用 mutex_, file_(file), formatter_(details::make_unique<spdlog::pattern_formatter>()) // 默认的 formatter
{
#ifdef _WIN32 // Windows 平台// 从 FILE* 对象获取 Windows 句柄handle_ = reinterpret_cast<HANDLE>(::_get_osfhandle(::_fileno(file_))); // 获取文件对应句柄// 判断文件指针和句柄if (handle_ == INVALID_HANDLE_VALUE && file != stdout && file != stderr){throw_spdlog_ex("spdlog::stdout_sink_base: _get_osfhandle() 失败", errno);}
#endif
}
stdout_sink_base 并非 base_sink 的派生类,因此无需实现 sink_it_ 和 flush_,但需要实现 sink 类的纯虚函数:log、flush、set_pattern 和 set_formatter。
template<typename ConsoleMutex>
SPDLOG_INLINE void stdout_sink_base<ConsoleMutex>::log(const details::log_msg &msg)
{
#ifdef _WIN32 // Windows 平台if (handle_ == INVALID_HANDLE_VALUE){return;}std::lock_guard<mutex_t> lock(mutex_); // 获取锁memory_buf_t formatted;formatter_->format(msg, formatted); // 格式化日志消息::fflush(file_); // 刷新文件缓冲区auto size = static_cast<DWORD>(formatted.size());DWORD bytes_written = 0;bool ok = ::WriteFile(handle_, formatted.data(), size, &bytes_written, nullptr) != 0;if (!ok){throw_spdlog_ex("stdout_sink_base: WriteFile() 失败. GetLastError(): " + std::to_string(::GetLastError()));}
#else // 非 Windows 平台std::lock_guard<mutex_t> lock(mutex_);memory_buf_t formatted;formatter_->format(msg, formatted);::fwrite(formatted.data(), sizeof(char), formatted.size(), file_);::fflush(file_); // 刷新每行到终端
#endif
}
flush 函数直接调用系统的 fflush 函数,set_pattern 函数通过模式字符串构造一个 pattern_formatter,
template<typename ConsoleMutex>
SPDLOG_INLINE void stdout_sink_base<ConsoleMutex>::flush()
{std::lock_guard<mutex_t> lock(mutex_);fflush(file_);
}
template<typename ConsoleMutex>
SPDLOG_INLINE void stdout_sink_base<ConsoleMutex>::set_pattern(const std::string &pattern)
{std::lock_guard<mutex_t> lock(mutex_);formatter_ = std::unique_ptr<spdlog::formatter>(new pattern_formatter(pattern));
}
template<typename ConsoleMutex>
stdout_sink_base 通过 mutex_ 实现线程安全。在每个需要保护数据的公共接口中,都会对 mutex_ 加锁。mutex_ 的类型是 mutex_t &(即 ConsoleMutex::mutex_t),从模板参数 ConsoleMutex 获取的。ConsoleMutex::mutex_t 可以是线程安全的互斥锁(如 console_mutex),也可以是非线程安全的空锁(如 console_nullmutex)。
控制台不同于普通文件,一个进程通常只有一个全局的控制台用于输出,因此所有的 stdout_sink_base 及其派生类共用一个控制台,也就需要共用一个互斥锁。
struct console_mutex
{using mutex_t = std::mutex; // 标准库互斥锁static mutex_t &mutex(){static mutex_t s_mutex; // 确保全局共享一个锁return s_mutex;}
};
struct console_nullmutex
{using mutex_t = null_mutex; // 自定义空锁static mutex_t &mutex(){static mutex_t s_mutex; // 确保全局共享一个锁return s_mutex;}
};
这篇关于spdlog日志库源码:输出通道sink的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!