spdlog一个非常好用的C++日志库(四): 源码分析之logger类

目录

1.简介

2.类图关系

3.logger数据成员

4.logger函数成员

4.1.构造与析构

4.1.1.构造函数

4.1.2.拷贝构造、移动构造

4.2.交换操作

4.3.log()记录日志消息

4.3.1.格式串

4.3.2.普通字符串

4.3.3.日志级别

4.3.4.宽字符支持

4.4.sink_it_:将log消息交给sink对象

4.5.写日志控制

5.线程安全

5.1.backtracer类

6.错误处理

7.logger类应用

7.1.创建logger对象

7.1.1.同步工厂方法synchronous_factory

7.1.2.异步工厂方法

7.2.获取logger对象

7.3.使用logger对象

7.4.删除logger对象

8.async_logger类

8.1.async_logger数据成员

8.2.async_logger构造与析构

8.3.async_logger的clone

8.4.async_logger前端接收log消息

8.5.async_logger后端写log消息


1.简介

一个logger类对象代表一个日志记录器,为用户提供日志记录接口。包括哪些功能?
基本功能:

  • logger名称,用于唯一标识该logger
  • 日志等级
  • 接收用户日志消息的接口
  • 提供一个sink(目标文件)指针数组和formatter(格式化),用于转换格式串并写到目标文件
  • 线程安全
  • 错误处理

高级功能:

  • 环形队列缓存最近消息,便于回溯
  • 自定义错误处理

2.类图关系

与logger有关的类图关系示意图:

log_msg 包含了logger名称、日志等级、记录log时间点、调用处信息,以及负责用户log消息等等,是一条log消息的原始组成部分;
source_loc 包含调用处的文件名、函数名、行数信息;

synchronous_factory 同步工厂,并非logger成员,用于创建非线程安全版本的logger对象;
async_factory,async_factory_nonblock,异步工厂,有2个版本,决定了当线程池缓冲区满时的策略,是阻塞等待 or 丢弃最老的,用于创建线程安全版本的logger对象;

sink 负责将log_msg转换为最终的log字符串,然后写入指定的目标文件。

3.logger数据成员

每个logger都拥有一个名字,全局注册表registry使用logger name来区分不同的logger对象,因此每个logger name应该不同。
一个logger对象包含多个sink对象,是因为用户可能需要一份log消息写到多个目标文件上,而一个sink对象代表了一个输出文件。

为何会有2个level_t日志等级成员(level_, flush_level_)?
设置2个level_t类型日志等级成员,是为了更精细化控制log消息。
当log消息log_msg的日志等级 > level_时,允许log消息写到目标文件(sink);
当log消息log_msg的日志等级 > flush_level_时,允许log消息flush(冲刷)到目标文件(sink);

当记录日志时,spdlog不会抛出异常。但构造logger或sink对象时,可能发生异常,这被认为是致命的。如果一个错误发生在记录日志时,默认情况下,库将打印一个错误信息到stderr。而custom_err_handler_是便于用户修改默认的错误处理。

tracer_ 用一个环形队列,记录最近的几个log message,当用户想要回溯时,tracer_ 便是一个好的选择。

protected:std::string name_;                          // logger名字std::vector<sink_ptr> sinks_;               // logger接收器(多个)spdlog::level_t level_{level::info};        // 记录日志等级, 决定是否允许log log_msgspdlog::level_t flush_level_{level::off};   // flush日志等级, 决定是否允许flush log_msgerr_handler custom_err_handler_{nullptr};   // 用户自定义错误处理回调details::backtracer tracer_;                // 回溯最近的一些log message, 使用环形队列存储             // 回溯最近的一些log message, 使用环形队列存储

这些成员设为protected,是允许派生类直接访问。

4.logger函数成员

4.1.构造与析构

4.1.1.构造函数

根据是否传入sink对象,构造函数分为两类:
1)空sink对象;
2)由调用者传入若干sink对象;

第2)种情况,sink对象有多种形式:单个sink对象、迭代器表示的范围、初始化列表。

public:// Empty loggerexplicit logger(std::string name): name_(std::move(name)), sinks_(){}// Logger with range on sinkstemplate<typename It>logger(std::string name, It begin, It end): name_(std::move(name)), sinks_(begin, end){}// Logger with single sinklogger(std::string name, sink_ptr single_sink): logger(std::move(name), {std::move(single_sink)}){}// Logger with sinks init listlogger(std::string name, sinks_init_list sinks): logger(std::move(name), sinks.begin(), sinks.end()){}virtual ~logger() = default;logger(const logger &other);logger(logger &&other) SPDLOG_NOEXCEPT;logger &operator=(logger other) SPDLOG_NOEXCEPT;

析构函数使用default(编译器自动合成的),virtual析构函数意味着该类可能会被继承。

4.1.2.拷贝构造、移动构造

// public methods
// copy ctor
SPDLOG_INLINE logger::logger(const logger &other): name_(other.name_), sinks_(other.sinks_), level_(other.level_.load(std::memory_order_relaxed)), flush_level_(other.flush_level_.load(std::memory_order_relaxed)), custom_err_handler_(other.custom_err_handler_), tracer_(other.tracer_)
{}// move ctor
SPDLOG_INLINE logger::logger(logger &&other) SPDLOG_NOEXCEPT: name_(std::move(other.name_)), sinks_(std::move(other.sinks_)), level_(other.level_.load(std::memory_order_relaxed)), flush_level_(other.flush_level_.load(std::memory_order_relaxed)), custom_err_handler_(std::move(other.custom_err_handler_)), tracer_(std::move(other.tracer_))
{}// operator=
SPDLOG_INLINE logger &logger::operator=(logger other) SPDLOG_NOEXCEPT
{this->swap(other);return *this;
}

什么时候使用直接构造,什么时候使用移动构造(std::move)?
移动操作的目的是避免即将释放的对象重复构造,也就是说,如果一个对象即将释放,用它来构造另一个对象的行为就可以改成移动构造。

4.2.交换操作

交换操作并没有使用通用的std::swap,因为通用的swap会构造一个新的临时对象,然后再赋值。因此,通用的swap操作是最后选择。

对于基本类型,swap操作是直接赋值;
对于对象类型,优先调用对象的swap成员函数,最后才是调用通用swap操作;
对于原子类型,使用专用的赋值或者交换函数;

// swap成员函数
SPDLOG_INLINE void logger::swap(spdlog::logger &other) SPDLOG_NOEXCEPT
{name_.swap(other.name_);sinks_.swap(other.sinks_);// swap level_auto other_level = other.level_.load();auto my_level = level_.exchange(other_level);other.level_.store(my_level);// swap flush level_other_level = other.flush_level_.load();my_level = flush_level_.exchange(other_level);other.flush_level_.store(my_level);custom_err_handler_.swap(other.custom_err_handler_);std::swap(tracer_, other.tracer_);
}// 重载swap函数
SPDLOG_INLINE void swap(logger &a, logger &b)
{a.swap(b);
}

4.3.log()记录日志消息

记录日志消息操作的目的是接受用户输入的log消息,构造一个log_msg对象,然后交给所拥有的每个sink对象,从而将log消息写到目标文件上。

4.3.1.格式串

由于需要支持参数不定的格式字符串,spdlog使用变长模板来支持这一特性。

    // 参数完整的记录日志接口// 用户输入的是变长参数argstemplate<typename... Args>void log(source_loc loc, level::level_enum lvl, format_string_t<Args...> fmt,  Args &&... args){log_(loc, lvl, fmt, std::forward<Args>(args)...); // 转发给private接口log_}

为了简化接口,spdlog使用一组参数使用了默认值的log的重载函数,为用户提供记录日志接口。它们都调用了参数完整版的log<...>()

    template<typename... Args>void log(level::level_enum lvl, format_string_t<Args...> fmt, Args &&... args){log(source_loc{}, lvl, fmt, std::forward<Args>(args)...); // source_loc为空}template<typename T>void log(level::level_enum lvl, const T &msg){log(source_loc{}, lvl, msg); // source_loc为空, T类型能转换为格式串}// T cannot be statically converted to format string (including  string_view/wstring_view)template<class T, typename  std::enable_if<!is_convertible_to_any_format_string<const T &>::value, int>::type  = 0>void log(source_loc loc, level::level_enum lvl, const T &msg){log(loc, lvl, "{}", msg); // source_loc为空, T类型不能转换为格式串, 直接将其转换为字符串}

可以看出,变长参数的log其实是交给log_的来实现的,而

    // common implementation for after templated public api has been resolvedtemplate<typename... Args>void log_(source_loc loc, level::level_enum lvl, string_view_t fmt, Args &&...  args){bool log_enabled = should_log(lvl);         // 只有优先级不低于指定优先级的log消息, 才被允许记录bool traceback_enabled = tracer_.enabled(); // 是否允许回溯最近的log消息if (!log_enabled && !traceback_enabled){return;}SPDLOG_TRY{memory_buf_t buf; // 二进制缓存
#ifdef SPDLOG_USE_STD_FORMATfmt_lib::vformat_to(std::back_inserter(buf), fmt,  fmt_lib::make_format_args(std::forward<Args>(args)...));
#else// seems that fmt::detail::vformat_to(buf, ...) is ~20ns faster than  fmt::vformat_to(std::back_inserter(buf),..)fmt::detail::vformat_to(buf, fmt,  fmt::make_format_args(std::forward<Args>(args)...));
#endifdetails::log_msg log_msg(loc, name_, lvl, string_view_t(buf.data(),  buf.size()));log_it_(log_msg, log_enabled, traceback_enabled);}SPDLOG_LOGGER_CATCH(loc)}// protected methods
SPDLOG_INLINE void logger::log_it_(const spdlog::details::log_msg &log_msg, bool  log_enabled, bool traceback_enabled)
{if (log_enabled){sink_it_(log_msg); // 将log_msg交给sink}if (traceback_enabled){tracer_.push_back(log_msg); // 环形队列缓存log_msg}
}

从log_实现上,可以看出,变长模板的处理,最终是交给ftm库的vformat_to函数处理了。

4.3.2.普通字符串

上面是处理的格式串,如果普通字符串也这样处理,效率会很低。logger类提供了更高效的方法。

    // 用户输入的是普通字符串string_view_tvoid log(source_loc loc, level::level_enum lvl, string_view_t msg){bool log_enabled = should_log(lvl);bool traceback_enabled = tracer_.enabled();if (!log_enabled && !traceback_enabled){return;}details::log_msg log_msg(loc, name_, lvl, msg);log_it_(log_msg, log_enabled, traceback_enabled);}// 简化版, 调用者无需指定source_locvoid log(level::level_enum lvl, string_view_t msg){log(source_loc{}, lvl, msg);}

如果用户想要指定log时间,logger也提供了对应接口

    // 调用者可以指定log时间点void log(log_clock::time_point log_time, source_loc loc, level::level_enum  lvl, string_view_t msg){bool log_enabled = should_log(lvl);         // 使能log levelbool traceback_enabled = tracer_.enabled(); // 使能回溯if (!log_enabled && !traceback_enabled){return;}details::log_msg log_msg(log_time, loc, name_, lvl, msg);log_it_(log_msg, log_enabled, traceback_enabled);}

4.3.3.日志级别

如果用户不想每次写log,都带上一个log level参数,该怎么办?
logger针对所有log level提供了一组写log消息的接口。

    // 针对各种日志级别的写log接口template<typename... Args>void trace(format_string_t<Args...> fmt, Args &&... args){log(level::trace, fmt, std::forward<Args>(args)...);}template<typename... Args>void debug(format_string_t<Args...> fmt, Args &&... args){log(level::debug, fmt, std::forward<Args>(args)...);}template<typename... Args>void info(format_string_t<Args...> fmt, Args &&... args){log(level::info, fmt, std::forward<Args>(args)...);}template<typename... Args>void warn(format_string_t<Args...> fmt, Args &&... args){log(level::warn, fmt, std::forward<Args>(args)...);}template<typename... Args>void error(format_string_t<Args...> fmt, Args &&... args){log(level::err, fmt, std::forward<Args>(args)...);}template<typename... Args>void critical(format_string_t<Args...> fmt, Args &&... args){log(level::critical, fmt, std::forward<Args>(args)...);}

针对不同日志级别的极简版写log接口,支持log消息类型为模板参数T,但要求能转换为string_view_t。这类接口特点是只需用户提供log消息,而且无需是string_view_t,只需要能隐式转换即可;选择调用的接口本身,就代表了日志级别。

    template<typename T>void trace(const T &msg){log(level::trace, msg); // msg必须能转换为string_view_t类型, 否则编译报错}template<typename T>void debug(const T &msg){log(level::debug, msg);}template<typename T>void info(const T &msg){log(level::info, msg);}template<typename T>void warn(const T &msg){log(level::warn, msg);}template<typename T>void error(const T &msg){log(level::err, msg);}template<typename T>void critical(const T &msg){log(level::critical, msg);}

4.3.4.宽字符支持

Windows下,可能使用宽字符问题,通过宏定义SPDLOG_WCHAR_TO_UTF8_SUPPORT来控制。logger提供了对应接口,跟非宽字符版本区别是:将format_string_t替换为wformat_string_t,将string_view_t替换为wstring_view_t。

例如,

    template<typename... Args>void log(source_loc loc, level::level_enum lvl, wformat_string_t<Args...> fmt,  Args &&... args){log_(loc, lvl, fmt, std::forward<Args>(args)...);}

宽字符版本,最终也是通过非宽字符版本实现的,中间多了个一个宽字符串到非宽字符串的转换:

    // 注意与非宽字符版本区别:msg参数类型为wstring_view_t, 实现上多了宽字符转换void log(log_clock::time_point log_time, source_loc loc, level::level_enum  lvl, wstring_view_t msg){bool log_enabled = should_log(lvl);bool traceback_enabled = tracer_.enabled();if (!log_enabled && !traceback_enabled){return;}memory_buf_t buf;details::os::wstr_to_utf8buf(wstring_view_t(msg.data(), msg.size()), buf); // 将宽字符转换为utf8字符details::log_msg log_msg(log_time, loc, name_, lvl,  string_view_t(buf.data(), buf.size()));log_it_(log_msg, log_enabled, traceback_enabled);}

4.4.sink_it_:将log消息交给sink对象

前面log_it_提到,log level使能(优先级符合要求)时,将构造的log消息对象交给sink对象。

// protected methods
SPDLOG_INLINE void logger::sink_it_(const details::log_msg &msg)
{for (auto &sink : sinks_) // logger拥有一个sink列表, 每个sink对象都有机会得到该log_msg对象{if (sink->should_log(msg.level)){SPDLOG_TRY{sink->log(msg);}SPDLOG_LOGGER_CATCH(msg.source)}}if (should_flush_(msg)) // 根据flush_level_判断是否允许flush{flush_();}
}

4.5.写日志控制

有2个控制接口:
should_log,控制是否允许写用户传入的log消息,采用策略是log消息本身级别(用户指定) >= logger指定的日志级别(创建者指定);
should_backtrace,控制是否允许回溯log消息,回溯策略是开启了该功能时,在写log消息同时,会将log消息加入到回溯用的环形队列tracer_中。

    // return true logging is enabled for the given level.bool should_log(level::level_enum msg_level) const{return msg_level >= level_.load(std::memory_order_relaxed);}// return true if backtrace logging is enabled.bool should_backtrace() const{return tracer_.enabled();}

还有一个私有的控制接口should_flush_,用来控制是否允许冲刷log消息。其策略类似于shoud_log。

SPDLOG_INLINE bool logger::should_flush_(const details::log_msg &msg)
{auto flush_level = flush_level_.load(std::memory_order_relaxed);return (msg.level >= flush_level) && (msg.level != level::off);
}

5.线程安全

严格来说,logger类本身并不提供线程安全保证,其线程安全是通过数据成员实现的。

name_通常构造时决定,后续不修改,但程序并未提供这种保证,因为是non-const。
level_和flush_level_都是level_t类型(即原子类型),无需考虑线程安全,需要考虑原子操作顺序,即内存布局。logger中,这2个原子变量内存布局都是松散的(std::memory_order_relaxed)。

#if defined(SPDLOG_NO_ATOMIC_LEVELS)
using level_t = details::null_atomic_int;
#else
using level_t = std::atomic<int>;
#endif

custom_err_handler_类型是std::function<void(const std::string &err_msg)>,并不提供线程安全保证,但又提供了set接口(set_error_handler),因此,该成员的访问不是线程安全的。

sinks_ 是std::vector<sink_ptr>,其线程安全依赖于sink类。sink类是一个抽象类,其线程安全依赖于派生类。spdlog中sink派生类,通过模板参数Mutex来决定锁类型,这为一套代码实现两套方案:无锁(_st)和有锁(_mt)提供支持。会提供专门的一文来讲解sink类。
tracer_是backtracer类型,其线程安全依赖于backtracer类。

5.1.backtracer类

backtracer类通过一个固定大小的环形队列messages_缓存最近log消息,为logger实现回溯log消息。向backtracer插入(push_back)前,必须通过enable()指定环形队列大小,否则环形队列messages_大小为0,无法插入数据。

class SPDLOG_API backtracer
{mutable std::mutex mutex_;            // 互斥锁std::atomic<bool> enabled_{false};    // backtracer使能状态circular_q<log_msg_buffer> messages_; // 环形队列public:backtracer() = default; // default ctorbacktracer(const backtracer &other); // copy ctorbacktracer(backtracer &&other) SPDLOG_NOEXCEPT; // move ctorbacktracer &operator=(backtracer other); // operator=void enable(size_t size); // 使能backtracer功能, 为环形队列指定大小void disable();           // 禁用backtracer, 但不会清除环形队列大小bool enabled() const;     // 返回backtracer使能状态void push_back(const log_msg &msg); // 向环形队列末尾插入一条log消息// pop all items in the q and apply the given fun on each of them.void foreach_pop(std::function<void(const details::log_msg &)> fun);
};

backtracer使用环形队列有2个比较重要的操作:push_back,向环形队列尾部插入一条log消息。当队列满时,并没有用阻塞等待的策略,而是用的默认的丢弃最老的log消息;
foreach_pop,逐条从环形队列头弹出log消息,并对每个弹出的log消息应用指定的fun函数。通过这种方式,让用户有机会对环形队列中的log消息进行处理。

foreach_pop代码如下:

// pop all items in the q and apply the given fun on each of them.
SPDLOG_INLINE void backtracer::foreach_pop(std::function<void(const  details::log_msg &)> fun)
{std::lock_guard<std::mutex> lock{mutex_};// 从队列messages_ 头逐个弹出log消息,并作为fun参数进行调用while (!messages_.empty()){auto &front_msg = messages_.front();fun(front_msg);messages_.pop_front();}
}

logger的转储dump_backtrace_()功能,就是用到了backtracer::foreach_pop,将环形队列中每条log消息都交给sink写到目标文件。该功能对于排查问题时,查看最近的log消息十分有用。

SPDLOG_INLINE void logger::dump_backtrace_()
{using details::log_msg;if (tracer_.enabled()){sink_it_(log_msg{name(), level::info, "****************** Backtrace Start  ******************"});tracer_.foreach_pop([this](const log_msg &msg) { this->sink_it_(msg); });sink_it_(log_msg{name(), level::info, "****************** Backtrace End  ********************"});}
}

6.错误处理

logger类定义了错误回调函数err_handler_,也提供用户自定义错误回调custom_err_handler_。缺省的错误处理方式是,向stderr打印一条错误信息,包含错误发生时间、错误计数、logger名称、错误正文消息等。

SPDLOG_INLINE void logger::err_handler_(const std::string &msg)
{if (custom_err_handler_) // 自定义错误处理回调{custom_err_handler_(msg);}else // 缺省的错误处理{using std::chrono::system_clock;static std::mutex mutex;static std::chrono::system_clock::time_point last_report_time;static size_t err_counter = 0;  // 错误计数器std::lock_guard<std::mutex> lk{mutex};auto now = system_clock::now(); // 错误发生时间点err_counter++;if (now - last_report_time < std::chrono::seconds(1)) // 确保两次错误时间间隔不会太小,以至于错误信息充斥屏幕{return;}last_report_time = now;auto tm_time = details::os::localtime(system_clock::to_time_t(now));char date_buf[64];std::strftime(date_buf, sizeof(date_buf), "%Y-%m-%d %H:%M:%S", &tm_time);
#if defined(USING_R) && defined(R_R_H) // if in R environmentREprintf("[*** LOG ERROR #%04zu ***] [%s] [%s] {%s}\n", err_counter,  date_buf, name().c_str(), msg.c_str());
#elsestd::fprintf(stderr, "[*** LOG ERROR #%04zu ***] [%s] [%s] {%s}\n",  err_counter, date_buf, name().c_str(), msg.c_str());
#endif}
}

什么时候可能发生错误?发生何种错误?
言外之意,就是什么时候由谁调用logger::err_handler_。logger中,err_handler_的调用实际上封装到了捕获异常的宏定义SPDLOG_LOGGER_CATCH中,

#ifndef SPDLOG_NO_EXCEPTIONS
#    define SPDLOG_LOGGER_CATCH(location)  \catch (const std::exception &ex)   \{                                  \if (location.filename)         \{                              \err_handler_(fmt_lib::format(SPDLOG_FMT_STRING("{} [{}({})]"),  ex.what(), location.filename, location.line));  \}                              \else                           \{                              \err_handler_(ex.what());   \}                              \}                                  \catch (...)                        \{                                  \err_handler_("Rethrowing unknown exception in logger");  \throw;                         \}
#else
#    define SPDLOG_LOGGER_CATCH(location)
#endif

而logger中需要用SPDLOG_LOGGER_CATCH捕获异常的,只有log_和sink_it。
对于log_,在尝试将log消息对应的格式串转换为普通string_view_t,以及利用sink写文件时(sink_it)。而调用log_()的,只有用户通过log()接口写入log消息的时候。
对于sink_it,在尝试格式化log消息,写文件的时候。

7.logger类应用

7.1.创建logger对象

在spdlog中,用户并不直接创建logger对象,而是通过工厂方法根据不同的sink,来创建logger对象。例如,下面代码用工厂方法创建一个logger对象:

// Create and return a shared_ptr to a multithread console logger.
#include "spdlog/sinks/stdout_color_sinks.h"
auto console = spdlog::stdout_color_mt("some_unique_name");

函数模板stdout_color_mt的定义是这样的:

// stdout_color_mt声明, 模板参数Factory默认使用同步工厂synchronous_factory
template<typename Factory = spdlog::synchronous_factory>
std::shared_ptr<logger> stdout_color_mt(const std::string &logger_name, color_mode  mode = color_mode::automatic);// stdout_color_mt定义, 使用工厂方法创建logger对象
template<typename Factory>
SPDLOG_INLINE std::shared_ptr<logger> stdout_color_mt(const std::string  &logger_name, color_mode mode)
{return Factory::template create<sinks::stdout_color_sink_mt>(logger_name,  mode);
}

7.1.1.同步工厂方法synchronous_factory

通常,一个工厂方法创建一种对象,如果想创建不同类型的对象,就传入参数,工厂方法内部进行判断后创建不同类型对象。synchronous_factory的精妙之处在于,函数参数用来创建对象,模板参数用来指定要创建的类型(有关的部分)。

logger name对于registry全局注册表来说,是唯一标识logger对象的。

这里有一个潜在的约定,所有工厂方法必须实现一个static create方法,通过模板参数Sink创建不同类型Sink派生类对象,然后绑定到新建的logger对象,从而实现不同的功能。

// Default logger factory-  creates synchronous loggers
class logger;struct synchronous_factory
{template<typename Sink, typename... SinkArgs>static std::shared_ptr<spdlog::logger> create(std::string logger_name,  SinkArgs &&... args){auto sink = std::make_shared<Sink>(std::forward<SinkArgs>(args)...); // 模板参数Sink决定了要具体Sink类型auto new_logger = std::make_shared<spdlog::logger>(std::move(logger_name),  std::move(sink)); // 用logger name及sink来创建logger对象details::registry::instance().initialize_logger(new_logger); // 初始化logger, 并添加到全局注册表return new_logger;}
};

7.1.2.异步工厂方法

针对所使用的环形队列,当队列满时,如果插入数据,有两种策略:阻塞、非阻塞,分别对应工厂类型async_factory、async_factory_nonblock。

using async_factory = async_factory_impl<async_overflow_policy::block>;  // 阻塞策略
using async_factory_nonblock =  async_factory_impl<async_overflow_policy::overrun_oldest>;  // 非阻塞策略

可以看到上面2种工厂类型,都是通过async_factory_impl来实现的。那么,async_factory_impl是如何实现的呢?
async_factory_impl也遵循工厂方法的潜规则:提供static create方法,根据模板参数Sink创建不同类型sink对象并绑定到新建的logger对象。

// async logger factory - creates async loggers backed with thread pool.
// if a global thread pool doesn't already exist, create it with default queue
// size of 8192 items and single thread.
template<async_overflow_policy OverflowPolicy = async_overflow_policy::block>
struct async_factory_impl
{template<typename Sink, typename... SinkArgs>static std::shared_ptr<async_logger> create(std::string logger_name, SinkArgs  &&... args){auto &registry_inst = details::registry::instance();// 如果全局线程池不存在,就创建一个// create global thread pool if not already exists..auto &mutex = registry_inst.tp_mutex();std::lock_guard<std::recursive_mutex> tp_lock(mutex);auto tp = registry_inst.get_tp();if (tp == nullptr){tp =  std::make_shared<details::thread_pool>(details::default_async_q_size, 1U);registry_inst.set_tp(tp);}auto sink = std::make_shared<Sink>(std::forward<SinkArgs>(args)...);// 创建新async_logger对象同时, 绑定线程池auto new_logger = std::make_shared<async_logger>(std::move(logger_name),  std::move(sink), std::move(tp), OverflowPolicy);registry_inst.initialize_logger(new_logger);return new_logger;}

跟同步工厂方法最大的区别是:异步工厂方法,是依附于一个(registry单例管理的)全局线程池的。创建出来的logger对象真实类型是派生类async_logger。而async_logger通过一个弱指针指向线程池。

上面的只是工厂的类型,并非工厂方法。用户想要利用工厂方法创建对象,需要用到下面的create_async, create_async_nb方法:

// 采用阻塞策略的异步工厂方法
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)...);
}

在客户端,比如你想创建一个basic_logger_mt,即一个基本都用于多线程环境的async_logger,可以这样封装工厂方法,然后供APP调用:

// include/spdlog/sinks/basic_file_sink.h// 封装工厂方法,供APP调用
// factory functions
template<typename Factory = spdlog::synchronous_factory>
inline std::shared_ptr<logger> basic_logger_mt(const std::string &logger_name, const filename_t &filename, bool truncate =  false, const file_event_handlers &event_handlers = {})
{return Factory::template create<sinks::basic_file_sink_mt>(logger_name,  filename, truncate, event_handlers);
}// APP端创建async_logger对象
// spdlog::init_thread_pool(32768, 1); // queue with max 32k items 1 backing  thread.
auto async_file =  spdlog::basic_logger_mt<spdlog::async_factory>("async_file_logger",  "logs/async_log.txt");

总结一下,定义并使用工厂方法的方式:
1)定义工厂类,提供static create方法,根据模板参数决定绑定到logger对象的Sink类型,从而决定不同输出目标;
2)对于异步工厂方法,还要将线程池绑定到logger对象;
3)返回的最终都是共享指针管理的logger对象;
4)为工厂方法提供一个包装方法,指定具体的模板参数Sink类型;

7.2.获取logger对象

spdlog中,使用工厂方法创建的logger对象,会自动注册到全局注册表registry,便于查询、管理。可用spdlog::get()方法获取已注册的loggers。

例如,创建名为"some_logger"的logger对象,并用spdlog::get获取:

auto my_logger = spdlog::basic_logger_mt("some_logger"); // 使用默认的同步工厂方法
...
auto some_logger = spdlog::get("some_logger");

注意:spdlog::get获取的类型跟创建时类型一致,都是shared_ptr管理的logger对象。

7.3.使用logger对象

获取到logger对象后,就能调用对应public接口了,譬如调用trace/log等接口就可以写log消息了。

例如,下面代码往日志文件("logs/async_log.txt")写内容"Async message #a"

auto async_file = spdlog::basic_logger_mt<spdlog::async_factory>("async_file_logger", "logs/async_log.txt");
int a = 10;
async_file->info("Async message #{}", a);

7.4.删除logger对象

删除logger对象是registry内容,详细见讲解registry类的文章,此处简要描述下。全局注册表registry是logger对象的持有者,可调用registry::drop删除指定logger名称的logger,或者调用registry::drop_all删除所有的logger。
注意:logger对象是通过shared_ptr管理,注册到registry的map存储结构中,因此只能将其从map中删除,而不会立即释放对象,需要等到引用计数为0。

spdlog::registry::drop("some_logger"); // 删除logger名称为"some_logger"的logger对象spdlog::registry::drop_all();          // 删除所有logger对象

8.async_logger类

async_logger类是logger类的派生类,专门用于接收用户log消息,然后交给线程池异步写入目标文件。用户提交log消息的线程,称为前端线程;将log消息写到目标文件的线程,称为后端线程。

8.1.async_logger数据成员

async_logger并非线程池的创建者,而线程池会用到logger的共享指针,而该指针可能指向async_logger对象,因此,async_logger使用thread_pool的弱指针。

在通过线程池往环形队列添加log消息时,可以指明所需的阻塞策略。async_logger给了调用者在构造时,就指定阻塞策略的机会,通过数据成员overflow_policy_记录。

private:std::weak_ptr<details::thread_pool> thread_pool_;async_overflow_policy overflow_policy_;                // 环形队列满时 阻塞策略

8.2.async_logger构造与析构

async_logger最完整的构造函数,是利用了sink迭代器范围来构造:

public:// begin, end是指向sink的迭代器范围template<typename It>async_logger(std::string logger_name, It begin, It end,  std::weak_ptr<details::thread_pool> tp,async_overflow_policy overflow_policy = async_overflow_policy::block): logger(std::move(logger_name), begin, end), thread_pool_(std::move(tp)), overflow_policy_(overflow_policy){}

在此基础上,提供了2个便捷的构造接口,便于使用sink的初始化列表、单个sink对象来构造async_logger对象:

// 下面2个构造函数都是基于上面sink迭代器范围实现的
SPDLOG_INLINE spdlog::async_logger::async_logger(std::string logger_name, sinks_init_list sinks_list,  std::weak_ptr<details::thread_pool> tp, async_overflow_policy overflow_policy): async_logger(std::move(logger_name), sinks_list.begin(), sinks_list.end(),  std::move(tp), overflow_policy)
{}SPDLOG_INLINE spdlog::async_logger::async_logger(std::string logger_name, sink_ptr single_sink,  std::weak_ptr<details::thread_pool> tp, async_overflow_policy overflow_policy): async_logger(std::move(logger_name), {std::move(single_sink)},  std::move(tp), overflow_policy)
{}

async_logger的析构函数就直接使用编译器默认合成的。

8.3.async_logger的clone

clone实际上是基类logger中定义的virtual函数,async_logger对象的clone类似,不过构造的对象是async_logger类型,而非logger类型。

SPDLOG_INLINE std::shared_ptr<spdlog::logger>  spdlog::async_logger::clone(std::string new_name)
{auto cloned = std::make_shared<spdlog::async_logger>(*this);cloned->name_ = std::move(new_name);return cloned;
}

想一想:为什么不用copy构造,来构造一个新async_logger对象?
因为clone需要的是一个除了logger name不同,其他属性均相同点async_logger对象。

8.4.async_logger前端接收log消息

async_logger的主要任务是什么?
从前端线程接收用户log消息,然后将其交给线程池;线程池空闲时,会调用async_logger来处理当前log消息,将其写到目标文件(sink)。

protected方法sink_it_就是用于前端线程,将接收到的用户log消息转交给线程池;flush_是向线程池发送一条flush异步消息,通知线程池尽早将log消息写到目标文件。

// send the log message to the thread pool
SPDLOG_INLINE void spdlog::async_logger::sink_it_(const details::log_msg &msg)
{if (auto pool_ptr = thread_pool_.lock()){pool_ptr->post_log(shared_from_this(), msg, overflow_policy_); // 将log消息转交给线程池}else{throw_spdlog_ex("async log: thread pool doesn't exist anymore");}
}// send flush request to the thread pool
SPDLOG_INLINE void spdlog::async_logger::flush_()
{if (auto pool_ptr = thread_pool_.lock()){pool_ptr->post_flush(shared_from_this(), overflow_policy_); // 发送一条flush消息给线程池, 将缓存内容尽早flush到文件}else{throw_spdlog_ex("async flush: thread pool doesn't exist anymore");}
}

8.5.async_logger后端写log消息

backend_sink_it_和backend_flush_是运行于后端线程(线程池子线程),分别对应前端任务sink_it_和flush_。

// backend functions - called from the thread pool to do the actual job
SPDLOG_INLINE void spdlog::async_logger::backend_sink_it_(const details::log_msg  &msg)
{for (auto &sink : sinks_){if (sink->should_log(msg.level)){SPDLOG_TRY{sink->log(msg); // 将log消息交给sink对象,写到目标文件}SPDLOG_LOGGER_CATCH(msg.source)}}if (should_flush_(msg)) // 如果允许的话,自动在后端flush{backend_flush_();}
}SPDLOG_INLINE void spdlog::async_logger::backend_flush_()
{for (auto &sink : sinks_){SPDLOG_TRY{sink->flush(); // 通知sink冲刷缓存到文件}SPDLOG_LOGGER_CATCH(source_loc())}
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/866197.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

PLC工作原理

PLC&#xff08;可编程逻辑控制器&#xff09;的工作原理简述为&#xff1a;集中采样、集中输出、周期性循环扫描。 西门子PLC 一、集中采样 顺序读取所有输入端子的通断状态&#xff0c;并将所读取的信息存到输入映像寄存器中&#xff0c;此时输入映像寄存器被刷新&#xff…

三坐标测量机:柔性生产制造中的高精度测量解决方案

柔性生产制造是制造业的核心竞争力之一。它强调生产线的灵活性和适应性&#xff0c;以满足市场对产品多样化和个性化的需求。在当今快速变化的工业环境中&#xff0c;随着消费者对产品个性化和定制化需求的增加&#xff0c;柔性生产制造和三坐标测量机的结合&#xff0c;为智能…

尽量不写一行if...elseif...写出高质量可持续迭代的项目代码

背景 无论是前端代码还是后端代码&#xff0c;都存在着定位困难&#xff0c;不好抽离&#xff0c;改造困难的问题&#xff0c;造成代码开发越来越慢&#xff0c;此外因为代码耦合较高&#xff0c;总是出现改了一处地方&#xff0c;然后影响其他地方&#xff0c;要么就是要修改…

通讯录(C语言详细版)

1. 前言 通讯录是在动态顺序表的基础上实现的&#xff0c;其实就是顺序表的每个元素存储的不再是数字&#xff0c;而是存储一个联系人的结构体&#xff0c;所以如果有些小伙伴看不懂的话&#xff0c;可以移步参考一下动态顺序表的实现&#xff1a;顺序表&#xff08;C语言详细…

【C语言】auto 关键字

在C语言中&#xff0c;auto关键字用于声明局部变量&#xff0c;但它的使用已经变得很少见。事实上&#xff0c;从C99标准开始&#xff0c;auto关键字的默认行为就是隐含的&#xff0c;因此在大多数情况下无需显式使用它。 基本用法 在C语言中&#xff0c;auto关键字用于指定变…

SpringBoot应用配置桥接Prometheus入门

SpringBoot应用配置Prometheus步骤 SpringBoot应用依赖要求PrometheusGrafanaGrafana监控界面模板 SpringBoot应用依赖要求 <!-- 监控系统健康情况的工具 --> <dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot…

【3分钟准备前端面试】vue3

目录 Vue3比vue2有什么优势vue3升级了哪些重要功能生命周期变化Options APIComposition APIreftoRef和toRefstoReftoRefsHooks (代码复用)Vue3 script setupsetupdefineProps和defineEmitsdefineExposeVue3比vue2有什么优势 性能更好体积更小更好的TS支持更好的代码组织更好的逻…

104.二叉树的最大深度——二叉树专题复习

深度优先搜索&#xff08;DFS&#xff09;是一种常用的递归算法&#xff0c;用于解决树形结构的问题。在计算二叉树的最大深度时&#xff0c;DFS方法会从根节点开始&#xff0c;递归地计算左右子树的最大深度&#xff0c;然后在返回时更新当前节点所在路径的最大深度。 如果我…

每日复盘-20240704

今日关注&#xff1a; 20240704 六日涨幅最大: ------1--------300391--------- 长药控股 五日涨幅最大: ------1--------300391--------- 长药控股 四日涨幅最大: ------1--------300391--------- 长药控股 三日涨幅最大: ------1--------300391--------- 长药控股 二日涨幅最…

文心一言 VS 讯飞星火 VS chatgpt (295)-- 算法导论21.4 4题

四、利用练习 21.4-2 &#xff0c;请给出一个简单的证明&#xff0c;证明在一个不相交集合森林上使用按秩合并策略而不使用路径压缩策略的运行时间为 O(m lgn) 。21.4-2 的内容是&#xff1a;“证明&#xff1a;每个结点的秩最多为 ⌊lgn⌋ 。”。如果要写代码&#xff0c;请用…

vue模板语法v-html

模板语法v-html vue使用一种基于HTML的模板语法&#xff0c;使我们能够声明式的将其组件实例的数据绑定到呈现的DOM上&#xff0c;所有的vue模板都是语法层面的HTML&#xff0c;可以被符合规范的浏览器和HTML解释器解析。 一.文本插值 最基本的数据绑定形式是文本插值&#…

Kafka 为何如此之快?深度解析其背后的秘密

目录 前言 一、生产者 1. 异步发送 2. 多分区并行 3. 消息批量发送 4.支持消息压缩 二、存储端 1. 分区和副本 2. 页缓存 3. 磁盘顺序写入 4. 零拷贝技术 5. 稀疏索引 三、消费端 1. 消费者群组 2. 批量拉取 3. 高效的偏移量管理 4. 并行消费 总结 前言 Kafk…

CS算法(二)—— 斜视SAR点目标仿真

SAR成像专栏目录 我们按照Cumming教授所著的《合成孔径雷达成像——算法与实现》7.6节的点目标参数进行仿真,斜视角设置为8,中心斜距改为1000km。先放最终的仿真结果: 1. 参数配置 在中心点和中心的的上下左右方向设置5个点目标 : function para=config_sar_para_cumming(…

【python数据处理】— “2020-01-01 05:20:15“日期格式数据

文章目录 一、数据说明及目标二、实现方式1.提取date2.提取hour3.提取weekday4.提取month 一、数据说明及目标 数据说明 数据表有一列名为"datetime"表示时间数据&#xff0c;该列的数据格式是"2020-01-01 05:20:15"。 import pandas as pd datapd.read_e…

[数据集][目标检测]刀具匕首持刀检测数据集VOC+YOLO格式8810张1类别

数据集格式&#xff1a;Pascal VOC格式YOLO格式(不包含分割路径的txt文件&#xff0c;仅仅包含jpg图片以及对应的VOC格式xml文件和yolo格式txt文件) 图片数量(jpg文件个数)&#xff1a;8810 标注数量(xml文件个数)&#xff1a;8810 标注数量(txt文件个数)&#xff1a;8810 标注…

堆结构、堆排序

堆 是完全二叉树&#xff0c;类似这种样式的 而这种有右子节点&#xff0c;没左子节点的就不是完全二叉树 分为大根堆和小根堆 大根堆是二叉树里每一颗子树的父节点都是这颗子树里最大的&#xff0c;即每一棵子树最大值是头节点的值 小根堆相反 把数组中从0开始的一段数人…

Spring Bean生命周期

Bean生命周期&#xff1a; 创建 Bean 的实例&#xff1a;Bean 容器首先会找到配置文件中的 Bean 定义&#xff0c;然后使用 Java 反射 API 来创建 Bean 的实例。 Bean 属性赋值/填充&#xff1a;为 Bean 设置相关属性和依赖&#xff0c;例如Autowired 等注解注入的对象、Value…

强强联合!当RAG遇到长上下文,滑铁卢大学发布LongRAG,效果领先GPT-4 Turbo 50%

过犹不及——《论语先进》 大学考试时&#xff0c;有些老师允许带备cheet sheet&#xff08;忘纸条&#xff09;,上面记着关键公式和定义,帮助我们快速作答提高分数。传统的检索增强生成(RAG)方法也类似,试图找出精准的知识片段来辅助大语言模型(LLM)。 但这种方法其实有问题…

React@16.x(48)路由v5.x(13)源码(5)- 实现 Switch

目录 1&#xff0c;原生 Switch 的渲染内容2&#xff0c;实现 1&#xff0c;原生 Switch 的渲染内容 对如下代码来说&#xff1a; import { BrowserRouter as Router, Route, Switch } from "react-router-dom"; function News() {return <div className"p…

MySQL体系架构

1.1.MySQL的分支与变种 MySQL变种有好几个&#xff0c;主要有三个久经考验的主流变种&#xff1a;Percona Server&#xff0c;MariaDB和 Drizzle。它们都有活跃的用户社区和一些商业支持&#xff0c;均由独立的服务供应商支持。同时还有几个优秀的开源关系数据库&#xff0c;值…