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;}
};

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

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

相关文章

sqoop操作

介绍 sqoop是隶属于Apache旗下的, 最早是属于cloudera公司的,是一个用户进行数据的导入导出的工具, 主要是将关系型的数据库(MySQL, oracle...)导入到hadoop生态圈(HDFS,HIVE,Hbase...) , 以及将hadoop生态圈数据导出到关系型数据库中 操作 将数据从mysql中导入到HDFS中 1.全量…

Pytest框架中的Setup和Teardown功能

在 pytest 测试框架中&#xff0c;setup 和 teardown是用于在每个测试函数之前和之后执行设置和清理的动作&#xff0c;而pytest 实际上并没有内置的 setup 和 teardown 函数&#xff0c;而是使用了一些装饰器或钩子函数来实现类似的功能。 学习目录 钩子函数&#xff08;Hook…

详解 Scala 的模式匹配

一、基本语法 Scala 中的模式匹配类似于 Java 中的 switch 语法&#xff0c;但是功能更强大 /**[var a / def func() ] var_name match {case value1 > statement1case value2 > statement2case value3 > statement3case _ > defaultOP} */ object TestMatchCase…

Python——cv2 判断图像读取内容是否为空

import cv2 pic_path"xxx.jpg" imagecv2.imread(pic_path) if None image:print("图片为空") else:print("图片不为空")

ubuntu上zsh与bash切换

在Ubuntu上切换zsh和bash&#xff0c;你可以使用命令行。 切换到zsh: chsh -s $(which zsh)切换回bash: chsh -s $(which bash)注意&#xff1a;chsh命令可能需要你输入你的用户密码。 如果你想立即启动新shell&#xff0c;而不用重启&#xff0c;可以运行&#xff1a; ex…

java中的线程安全的容器

ReentrantLock 和 Condition 都是基于 AbstractQueuedSynchronizer (AQS) 实现的。 以下是 ReentrantLock 与 AQS 的关系&#xff1a; ReentrantLock 状态管理&#xff1a;ReentrantLock 通过 AQS 的 state 变量来表示锁的状态。状态为 0 表示锁未被持有&#xff0c;状态为 1…

【python】YOLO目标检测txt标签转xml,支持与原xml标签融合

需求&#xff1a; 利用现有yolo模型权重实现自动标注功能&#xff0c;即使用yolov5源码中的detect.py对待标注图像执行推理任务&#xff0c;并使用--save-txt保存标签文件&#xff0c;然后使用本文python代码直接将detect.py生成的txt标签转为xml标签&#xff0c;最后再使用la…

SAP PP学习笔记14 - MTS(Make-to-Stock) 按库存生产(策略10),以及生产计划的概要

上面讲了SAP里面的基础知识&#xff0c;BOM&#xff0c;作业手顺&#xff08;工艺路线&#xff09;&#xff0c;作业区&#xff08;工作中心&#xff09;&#xff0c;MRP&#xff0c;MPS等概念&#xff0c;现在该到用的时候了。 SAP PP学习笔记07 - 简单BOM&#xff0c;派生BO…

5门编程语言哪个好一点:深入剖析与选择指南

5门编程语言哪个好一点&#xff1a;深入剖析与选择指南 在编程的广阔领域中&#xff0c;各种语言如繁星般璀璨&#xff0c;各有其独特之处。对于初学者或寻求转型的开发者来说&#xff0c;如何选择一门合适的编程语言往往令人头疼。本文将针对五门热门编程语言进行详细剖析&am…

STC8增强型单片机进阶开发--独立按键

知不足而奋进 望远山而前行 文章目录 目录 文章目录 前言 目标 内容 原理图 按键消抖 软件设计 要求 分析 实现单个按钮 实现多个按钮 使用位操作存储状态 总结 前言 本次学习旨在探索按键操作及按键消抖的原理和实现方法。通过学习原理图、按键消抖的三种方法以及软件设计的要…

NXP RT1060学习总结 - 基础CAN功能

1、RT1060-CAN功能简介 这里使用的是RT1060系列的1064芯片进行开发&#xff0c;使用的是官方提供的开发板&#xff1b;提供的CAN外设为CAN2&#xff0c;使用CAN2的好处是IO与CAN3可以互相映射&#xff0c;而CAN3是具备CAN-FD功能。 2、CAN IO初始化 static void can2_gpio_c…

什么是Java泛型?它有什么作用

Java泛型&#xff08;Generics&#xff09;是一种允许在定义类、接口和方法时使用类型参数的机制。泛型提供了一种机制&#xff0c;使得代码可以对多种类型的对象进行操作&#xff0c;而无需进行类型转换。 Java泛型的作用 类型安全&#xff1a;通过在编译时进行类型检查&…

如何选择D类音频放大器(数字功率放大器)

1 简介 多年来&#xff0c;音频内容一直在不断发展。从当地唱片店购买 12 英寸 LP 黑胶唱片的时代已经成为过去&#xff0c;现在我们通过流式传输几乎可即时播放云端的任何内容。虽然一些音频爱好者会为了获得新奇体验而重拾黑胶唱片&#xff0c;但今天绝大多数的音频都是以数…

JVM学习笔记(持续更新)

JDK、JRE、JVM区别&#xff1f; 类加载过程 装载 验证 准备 解析 初始化 类加载器分类 双亲委派模型 如何打破双亲委派模型&#xff1f; 自定义类加载器&#xff0c;集成ClassLoader类重写loadClass,如Tomcat JVM内存模型 JVM 需要使用计算机的内存&#xff0c;Java 程序…

【LeetCode 101】对称二叉树

1. 题目 2. 分析 这道题比较经典。我又一次做错了&#xff0c;这次是花了20min都没有做出来。 最开始我的思想就是&#xff0c;递归比较左根节点的左子树和右根节点的右子树是否对称即可&#xff0c;然后觉得能解决问题了&#xff0c;便动手coding。哪知道&#xff0c;又碰到了…

电源滤波器怎么选用

电源滤波器怎么选用 滤波器应用场景及作用第一步&#xff1a;第二步&#xff1a;第三步&#xff1a;第四步&#xff1a; 滤波器应用场景及作用 可以有效解决EMC测试无法通过、端口防护、滤除干扰、设备保护等问题 主要功能有: 1、降低主电源谐波; 2、保护驱动装置电力电子元件…

算法人生(18):从神经网络的“剪枝策略”看“怎么找回时间”

IT人的工作和生活难平衡这事&#xff0c;到底要怎么解决呢&#xff0c;让我们从神经网络的“剪枝策略”中找点灵感吧&#xff01; 剪枝策略是指训练和优化深度神经网络时采取的一种技术&#xff0c;从名字就知道&#xff0c;它就像修剪树木一样&#xff0c;去除不必要的枝叶&a…

枣庄高防服务器如何确保数据的安全保护?

如何利用枣庄高防服务器确保数据的安全保护&#xff1f; 在当今信息时代&#xff0c;数据安全已经成为企业和个人都需要面对的重要问题。为了保障数据的安全&#xff0c;许多企业选择枣庄高防服务器&#xff0c;其强大的安全防护能力为用户提供了可靠的保障。而为了最大程度地…

stack和queue(2): 模拟实现

一、stack的模拟实现 stack是一个容器适配器&#xff0c;它的底层是通过对某种容器类进行封装来实现&#xff0c;标准容器list和vector&#xff0c;deque都符合这些需求&#xff0c;默认情况下&#xff0c;如果没有为stack指定底层容器就默认是使用deque实现。 我们在模拟实现…

Vuex 是什么?VueX简介

聚沙成塔每天进步一点点 本文内容 ⭐ 专栏简介Vuex 是什么核心概念1.State&#xff08;状态&#xff09;2. Getter&#xff08;获取器&#xff09;3. Mutation&#xff08;突变&#xff09;4. Action&#xff08;动作&#xff09;5. Module&#xff08;模块&#xff09; 原理解…