提示:这里可以添加系列文章的所有文章的目录,目录需要自己手动添加
例如:第一章 Python 机器学习入门之pandas的使用
提示:写完文章后,目录可以自动生成,如何生成可参考右边的帮助文档
示例中,周期性刷新时间的设置只演示了一个接口
spdlog::flush_every(std::chrono::seconds(3));
接口定义在spdlog.h 实现在spdlog-inl.h
SPDLOG_INLINE void flush_every(std::chrono::seconds interval)
{
details::registry::instance().flush_every(interval);
}
还是调用了registry的接口,我们关注得是这个值有什么用
registry中的该接口实现如下
SPDLOG_INLINE void registry::flush_every(std::chrono::seconds interval)
{
std::lock_guard lock(flusher_mutex_);
auto clbk = [this]() { this->flush_all(); };
periodic_flusher_ = details::make_unique(clbk, interval);
}
从代码中看到,只是创建了一个periodic_flusher_这个对象,类型为periodic_worker。看名字也知道是周期性的执行一个函数,函数就是clbk。
peridoic_worker是如何实现的呢?定义在periodic_worker.h。定义比较短,这里全贴出来
class SPDLOG_API periodic_worker
{
public:
periodic_worker(const std::function &callback_fun, std::chrono::seconds interval);
periodic_worker(const periodic_worker &) = delete;
periodic_worker &operator=(const periodic_worker &) = delete;
// stop the worker thread and join it
~periodic_worker();
private:
bool active_;
std::thread worker_thread_;
std::mutex mutex_;
std::condition_variable cv_;
};
可以看到,这里是新建了一个线程。我比较好奇的是,周期性执行一个函数,这里是采用什么方式实现的。当然通过类内的条件变量和锁,也能大致猜到。其在构造函数中实现,如下:
SPDLOG_INLINE periodic_worker::periodic_worker(const std::function &callback_fun, std::chrono::seconds interval)
{
active_ = (interval > std::chrono::seconds::zero()); // 时间间隔大于0秒才激活线程。
// 该变量是为停止该线程而设计的。如果没有该变量,线程将没有恰当的时机被终止。该变量无需保护。
if (!active_)
{
return;
}
worker_thread_ = std::thread([this, callback_fun, interval]() { //线程执行函数体
for (;;)
{
//先加锁,这是使用条件变量的标准流程,并不是为了保护哪个临界区
std::unique_lock lock(this->mutex_);
// 等待interval秒,期间如果有人notify,会提前返回。
// 当然我们知道就是为了让它等interval秒而已,正常情况下不会有人notify的
// 条件变量返回后,会判断后面的lamba表达式是否为真,如果是真,直接return。
// 这是用于判断线程是否该结束。
if (this->cv_.wait_for(lock, interval, [this] { return !this->active_; }))
{
return; // active_ == false, so exit this thread
}
callback_fun();
}
});
}
SPDLOG_INLINE void registry::flush_all()
{
std::lock_guard lock(logger_map_mutex_);
for (auto &l : loggers_)
{
l.second->flush();
}
}
可以看到只是调用了logger的flush()方法。
意料之中的是:logger又调用了sink的flush方法。记得logger并不直接处理日志输出目标,而是由sink代劳。
普通logger是由sink直接调用操作系统接口刷新的,例如
file_helper_.flush()
文件辅助类后续再学习
uml图
spdlog::enable_backtrace(32);
spdlog::debug("Backtrace message {}", i);
spdlog::dump_backtrace();
可以看到,trace功能是需要开启的,trace的记录数量也是需要设置的。
enable_trace同样也是调用了registry的接口,实现如下
SPDLOG_INLINE void registry::enable_backtrace(size_t n_messages)
{
std::lock_guard lock(logger_map_mutex_);
backtrace_n_messages_ = n_messages;
for (auto &l : loggers_)
{
l.second->enable_backtrace(n_messages);
}
}
可以看到,registry的接口,通过调用logger的enable_trace,把他保存的所有logger都设置了一样的trace。
而logger内部有一个tracer_类专门用于处理trace消息
// create new backtrace sink and move to it all our child sinks
SPDLOG_INLINE void logger::enable_backtrace(size_t n_messages)
{
tracer_.enable(n_messages);
}
而在输出日志时,会先询问tracer_要不要输出trace
log(....) {
bool traceback_enabled = tracer_.enabled();
...
}
那么默认情况下,logger会不会启用trace功能呢?
看代码发现,tracer_一般是默认初始化的,其类型为backtracer,实现在backetracer.h
class logger{
...
details::backtracer tracer_;
...
}
backtracer.h
class SPDLOG_API backtracer
{
...
std::atomic enabled_{false};
circular_q messages_;
public:
void enable(size_t size);
bool enabled() const;
...
};
SPDLOG_INLINE bool backtracer::enabled() const
{
return enabled_.load(std::memory_order_relaxed);
}
判断trace_是否启用的变量是原子的,默认为false。这里的使用很微妙,为什么是原子的呢?有待后续思考、学习。
其enabled()问询实现,也是没见过的用法:
backtracer-inl.h
SPDLOG_INLINE bool backtracer::enabled() const
{
return enabled_.load(std::memory_order_relaxed);
}
stopwatch应该是一个性能工具,可以方便的统计代码执行的用时
spdlog::stopwatch sw;
spdlog::debug("Elapsed {}", sw);
spdlog::debug("Elapsed {:.3}", sw);
stopwatch实现很简单,其中的时间戳大量使用了c++:chrono库的工具。c++对chrono时间工具的设计很全面,全面到有些复杂。这里贴两篇别人的介绍文档。
c++11 chrono全面解析(最高可达纳秒级别的精度)
C++11中的时间库std::chrono(引发关于时间的思考)
class stopwatch
{
using clock = std::chrono::steady_clock;
std::chrono::time_point start_tp_;
public:
stopwatch()
: start_tp_{clock::now()}
{}
// 到现在过去了多久。
std::chrono::duration elapsed() const
{
return std::chrono::duration(clock::now() - start_tp_);
}
// 重置一下时间检查点
void reset()
{
start_tp_ = clock::now();
}
};
根据其接口,容易理解其功能,已添加注释。
核心在于,该类如何与format进行交互。
这个我自己不太懂,所以贴在这里。看看系统日志到底是个什么东西。
auto syslog_logger = spdlog::syslog_logger_mt("syslog", ident, LOG_PID);
系统日志实现在:syslog_sink.h
template
class syslog_sink : public base_sink {};
构造函数
{
// set ident to be program name if empty
::openlog(ident_.empty() ? nullptr : ident_.c_str(), syslog_option, syslog_facility);
}
析构函数
{
::closelog();
}
sink_it_ 函数
{
....
::syslog(syslog_prio_from_level(msg), "%.*s", static_cast(length), payload.data());
}
原来系统日志就是调用了syslog.h中的接口。该头文件是The Single UNIX ® Specification, Version 2标准中规定的。文档
好奇心驱使,看看android日志怎么打印的。
auto android_logger = spdlog::android_logger_mt("android", tag);
android_sink实现在android_sink.h。其也是用了android提供的系统日志工具:android/log.h。不过它好像可以直接使用,不需要open和close。不过它好像可能输出失败,添加了重试的逻辑。
sink_it_(...) {
...
android_log(priority, tag_.c_str(), msg_output);
while ((ret == -11 /*EAGAIN*/) && (retry_count < SPDLOG_ANDROID_RETRIES))
{
details::os::sleep_for_millis(5);
ret = android_log(priority, tag_.c_str(), msg_output);
retry_count++;
}
...
}
android_log根据android的版本不同,提供了两个重载。
// There might be liblog versions used, that do not support __android_log_buf_write. So we only compile and link against
// __android_log_buf_write, if user explicitely provides a non-default log buffer. Otherwise, when using the default log buffer, always
// log via __android_log_write.
template
typename std::enable_if(log_id::LOG_ID_MAIN), int>::type android_log(int prio, const char *tag, const char *text)
{
return __android_log_write(prio, tag, text);
}
template
typename std::enable_if(log_id::LOG_ID_MAIN), int>::type android_log(int prio, const char *tag, const char *text)
{
return __android_log_buf_write(ID, prio, tag, text);
}
此外,为了统一spdlog的日志等级和android的日志等级,使用convert_to_android_来转换这两种日志等级
static android_LogPriority convert_to_android_(spdlog::level::level_enum level)
接下来的学习:
文件辅助类
文本格式化:fmt工具
线程池实现
等等,想到再补充