当前位置: 首页 > news >正文

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())}
}

相关文章:

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_&#xff1a;将log消息…...

逻辑这回事(七)---- 器件基础

Xilinx FPGA创建了先进的硅模块(ASMBL)架构,以实现FPGA具有针对不同应用程序领域优化的各种功能组合的平台。通过这一创新,Xilinx提供了更多的设备选择,使客户能够为其特定设计选择具有正确的功能和功能组合的FPGA。ASMBL体系结构通过以下方式突破了传统的设计障碍:消除几…...

中俄汽车产业链合作前景广阔,东方经济论坛助力双边合作与创新

随着中国汽车零部件企业的竞争力和创新能力不断增强&#xff0c;中国汽车及零部件行业在俄罗斯的市场份额和品牌影响力显著提升&#xff0c;中俄两国在汽车产业链上的合作展现出巨大的潜力和广阔的前景。2024年5月&#xff0c;俄罗斯乘用车新车销量达到12.8万辆&#xff0c;同比…...

第六篇:精通Docker Compose:打造高效的多容器应用环境

精通Docker Compose&#xff1a;打造高效的多容器应用环境 1. 引言 1.1 目的与重要性 在现代软件开发中&#xff0c;随着应用程序的复杂性不断增加&#xff0c;传统的单一容器部署方式已无法满足需求。Docker Compose作为一种强大的工具&#xff0c;专门用于定义和运行多容器…...

C++视觉开发 一.OpenCV环境配置

一.OpenCV安装环境配置 1.OpenCV安装 &#xff08;1&#xff09;下载 官方下载链接&#xff1a;http://opencv.org/releases 这边选择需要的版本&#xff0c;我是在windows下的4.9.0。&#xff08;科学上网下载很快&#xff0c;否则可能会有点慢&#xff09; (2)安装 双击下…...

大数据面试题之Kafka(3)

目录 Kafka支持什么语义&#xff0c;怎么实现ExactlyOnce? Kafka的消费者和消费者组有什么区别?为什么需要消费者组? Kafka producer的写入数据过程? Kafka producer的ack设署 Kafka的ack机制&#xff0c;解决了什么问题? Kafka读取消息是推还是拉的模式?有什…...

视频监控平台web客户端的免密查看视频页:在PC浏览器上如何调试手机上的前端网页(PC上的手机浏览器的开发者工具)

目录 一、手机上做前端页面开发调试 1、背景 2、视频监控平台AS-V1000的视频分享页 3、调试手机前端页面代码的条件 二、手机端的准备工作 1、手机准备 2、手机的开发者模式 3、PC和手机的连接 &#xff08;1&#xff09;进入调试模式 &#xff08;2&#xff09;选择…...

力扣2488.统计中位数为 K 的子数组

力扣2488.统计中位数为 K 的子数组 等价转换 子数组为奇数 &#xff1a; 左小 右小 左大 右大 左小 – 左大 右大 – 右小 子数组为偶数 &#xff1a; 左小 右小 左大 右大 – 1 左小 – 左大 右大 – 右小 - 1提示中说明k为两数中左边那个 先从k的下标pos开始往左逆序…...

Zabbix对接Elasticsearch(ES)数据库(未成功)

0.需求分析 不管zabbix的后端数据库是oracle还是mysql&#xff0c;当zabbix监控的量级达到了一定程度后&#xff0c;那么对数据库的性能是一个非常严峻的挑战。特别是对历史数据的查询&#xff0c;将会变得非常非常的慢&#xff0c;别告诉我可以建索引优化&#xff0c;当量级达…...

【unity实战】使用Unity实现动作游戏的攻击 连击 轻重攻击和打击感

最终效果 文章目录 最终效果前言素材下载&#xff1a;玩家移动跳跃控制攻击动画配置轻攻击重攻击 攻击时禁止移动和攻击移动补偿敌人击退和播放受击动画受击特效攻击停顿和屏幕震动局部顿帧&#xff08;补充&#xff09;参考源码完结 前言 注意本文为自己的学习记录笔记&#…...

ELK 企业实战7

ELKkafkafilebeat企业内部日志分析系统 1、组件介绍 1、Elasticsearch&#xff1a; 是一个基于Lucene的搜索服务器。提供搜集、分析、存储数据三大功能。它提供了一个分布式多用户能力的全文搜索引擎&#xff0c;基于RESTful web接口。Elasticsearch是用Java开发的&#xff…...

linux 下neo4j的安装

一、neo4j简介 Neo4j 是一个高性能的 NoSQL 图形数据库,它将结构化数据存储在网络(从数学角度叫做图)上而不是表中。Neo4j 也可以被看作是一个高性能的图引擎,该引擎具有成熟数据库的所有特性。 neo4j与jdk版本对应 neo4j的版本需要与jdk版本相适配,否则容易出现安装失…...

Flink ProcessFunction不同流异同及应用场景

ProcessFunction系列对比概览 函数类别关键特性应用场景示例ProcessFunction基础类&#xff0c;处理单个事件&#xff0c;支持事件时间、水位线、状态管理、定时器。单独处理每个事件&#xff0c;执行复杂逻辑&#xff0c;如基于事件内容动态响应。KeyedProcessFunction基于键…...

Matplotlib 文本

可以使用 xlabel、ylabel、text向图中添加文本 mu, sigma 100, 15 x mu sigma * np.random.randn(10000)# the histogram of the data n, bins, patches plt.hist(x, 50, densityTrue, facecolorg, alpha0.75)plt.xlabel(Smarts) plt.ylabel(Probability) plt.title(Histo…...

信创产业政策,信创测试方面

信创产业的政策支持主要体现在多个方面&#xff0c;这些政策旨在推动产业的快速发展&#xff0c;加强自主创新能力&#xff0c;保障国家信息安全&#xff0c;以及促进产业结构的优化升级。 首先&#xff0c;政府通过财政支持、税收优惠等方式&#xff0c;加大对信创产业的资金…...

微信云数据库迁移到unicloud云数据库

背景 早期只有一个微信小程序&#xff0c;后来了解到uniapp的跨端解决方案&#xff0c;开始从小程序代码迁移到uniapp。对于后端采用的微信云开发方案&#xff0c;迁移的时候主要要解决从openid的用户体系转移到unicloud提供的uni-id体系(使用uid)。 方案 利用微信云数据库的…...

快速上手文心一言指令

“文心一言”指的是百度公司开发的自然语言处理与生成技术&#xff0c;它类似于ChatGPT&#xff0c;是一种基于大规模语言模型的AI对话系统&#xff0c;能够理解和生成自然语言文本&#xff0c;进行问答、创作等多种任务。由于“文心一言”是一个复杂的系统&#xff0c;其内部指…...

零基础STM32单片机编程入门(五)FreeRTOS实时操作系统详解及实战含源码视频

文章目录 一.概要二.什么是实时操作系统三.FreeRTOS的特性四.FreeRTOS的任务详解1.任务函数定义2.任务的创建3.任务的调度原理 五.CubeMX配置一个FreeRTOS例程1.硬件准备2.创建工程3.调试FreeRTOS任务调度 六.CubeMX工程源代码下载七.讲解视频链接地址八.小结 一.概要 FreeRTO…...

leetCode.96. 不同的二叉搜索树

leetCode.96. 不同的二叉搜索树 题目思路 代码 // 方法一&#xff1a;直接用卡特兰数就行 // 方法二&#xff1a;递归方法 class Solution { public:int numTrees(int n) {// 这里把 i当成整个结点&#xff0c;j当成左子树最左侧结点,并一次当根节点尝试// f[ i ] f[ j - 1…...

PyAutoGUI 使用详解

文章目录 简介PyAutoGUI 的原理安装 PyAutoGUI基本使用示例鼠标控制键盘控制截屏图像识别消息框 高级功能防止误操作多屏幕支持鼠标平滑移动 结论 简介 PyAutoGUI 是一个用于自动化控制鼠标和键盘的 Python 库。它可以帮助开发者编写脚本&#xff0c;以模拟用户在计算机上的操…...

MySQL——备份

为什么要备份&#xff1f; 保证重要的数据不丢失 方便数据转移 MySQL数据库备份方式&#xff1a; 1. 直接拷贝物理文件 2. 在可视化工具中手动导出 —— 在想要导出的表或者库中&#xff0c;右键选择备份或导出 3. 使用命令行导出 mysqldump ——cmd打开命令行 —…...

科东软件精彩亮相华南工博会,展现未来工业前沿技术

近日&#xff0c;华南国际工业博览会在深圳成功举办。科东软件携众多前沿技术、解决方案及最新应用案例精彩亮相&#xff0c;为参展观众带来了一场工业智能的科技盛宴。 鸿道操作系统&#xff08;Intewell&#xff09; 科东软件重点展示了鸿道操作系统&#xff08;Intewell&…...

详解flink sql, calcite logical转flink logical

文章目录 背景示例FlinkLogicalCalcConverterBatchPhysicalCalcRuleStreamPhysicalCalcRule其它算子FlinkLogicalAggregateFlinkLogicalCorrelateFlinkLogicalDataStreamTableScanFlinkLogicalDistributionFlinkLogicalExpandFlinkLogicalIntermediateTableScanFlinkLogicalInt…...

PostgreSQL的系统视图pg_statio_all_indexes

PostgreSQL的系统视图pg_statio_all_indexes 在 PostgreSQL 数据库中&#xff0c;pg_statio_all_indexes 视图提供了有关所有索引的 I/O 活动的统计信息。这些统计信息对于了解索引的使用情况和性能调优非常有帮助。 pg_statio_all_indexes 视图的结构 以下是 pg_statio_all…...

【C++ Primer Plus学习记录】函数和C-风格字符串

将字符串作为参数时意味着传递的是地址&#xff0c;但可以使用const来禁止对字符串参数进行修改。 假设要将字符串作为参数传递给函数&#xff0c;则表示字符串的方式有三种&#xff1a; &#xff08;1&#xff09;char数组 &#xff08;2&#xff09;用引号括起来的字符串常…...

力扣双指针算法题目:移动零

1.题目 . - 力扣&#xff08;LeetCode&#xff09; 2.思路解析 这个题目的思路和“使用递归排序快速排序解决数组的排序问题”相同 class solution { public:void QuickSort(vector<int>& nums, int left, int right){if (left > right) return;int key left…...

day60---面试专题(微服务面试题-参考回答)

微服务面试题 **面试官&#xff1a;**Spring Cloud 5大组件有哪些&#xff1f; 候选人&#xff1a; 早期我们一般认为的Spring Cloud五大组件是 Eureka : 注册中心Ribbon : 负载均衡Feign : 远程调用Hystrix : 服务熔断Zuul/Gateway : 网关 随着SpringCloudAlibba在国内兴起 , …...

laravel+phpoffice+easyexcel实现导入

资源包下载地址 https://download.csdn.net/download/QiZong__BK/89503486 easy-excel下载&#xff1a; "dcat/easy-excel": "^1.0", 命令行&#xff1a; composer require dcat/easy-excel 前端代码 <!doctype html> <html lang"en&…...

Spring Boot集成多数据源的最佳实践

Spring Boot集成多数据源的最佳实践 大家好&#xff0c;我是免费搭建查券返利机器人省钱赚佣金就用微赚淘客系统3.0的小编&#xff0c;也是冬天不穿秋裤&#xff0c;天冷也要风度的程序猿&#xff01; 为什么需要多数据源&#xff1f; 在实际的应用开发中&#xff0c;有时候…...

Java项目:基于SSM框架实现的班主任助理管理系统【ssm+B/S架构+源码+数据库+开题报告+毕业论文】

一、项目简介 本项目是一套基于SSM框架实现的班主任助理管理系统 包含&#xff1a;项目源码、数据库脚本等&#xff0c;该项目附带全部源码可作为毕设使用。 项目都经过严格调试&#xff0c;eclipse或者idea 确保可以运行&#xff01; 该系统功能完善、界面美观、操作简单、功…...

数据在内存中的存储方式

&#x1f31f;&#x1f31f;作者主页&#xff1a;ephemerals__ &#x1f31f;&#x1f31f;所属专栏&#xff1a;C语言 目录 前言 一、整数的存储 二、大小端字节序及其判断 1.什么是大小端 2.为什么有大小端 3.用c语言编写程序判断大小端 三、浮点数的存储 1.浮点数…...

Selenium 监视数据收发

实际上&#xff0c;在我提供的示例中&#xff0c;确实使用了浏览器实例。webdriver.Chrome()这行代码正是创建了一个Chrome浏览器的WebDriver实例。Selenium Wire扩展了标准的Selenium WebDriver&#xff0c;允许你通过这个浏览器实例来监听网络请求。 当你运行类似这样的代码…...

基于 STM32 的智能睡眠呼吸监测系统设计

本设计的硬件构成&#xff1a; STM32F103C8T6单片机最小系统板&#xff08;包含3.3V稳压电路时钟晶振电路复位电路&#xff08;上电自复位&#xff0c;手动复位&#xff09;&#xff09;&#xff0c;心率传感器、气压传感器、液晶显示、按键、蜂鸣器、LED灯、蓝牙模块组合而成…...

Spring的事务管理、AOP实现底层

目录 spring的事务管理是如何实现的&#xff1f; Spring的AOP的底层实现原理 spring的事务管理是如何实现的&#xff1f; 首先&#xff0c;spring的事务是由aop来实现的&#xff0c;首先要生成具体的代理对象&#xff0c;然后按照aop的整套流程来执行具体的操作逻辑&#xff…...

基于SpringBoot的篮球竞赛预约平台

你好&#xff0c;我是计算机学姐码农小野&#xff01;如果你对篮球竞赛预约平台感兴趣或有相关需求&#xff0c;欢迎私信联系我。 开发语言&#xff1a; Java 数据库&#xff1a; MySQL 技术&#xff1a; SpringBootMySql 工具&#xff1a; MyEclipse、Tomcat 系统展示…...

学生用小台灯什么牌子的好?列举出几款学生用台灯推荐

眼睛是我们感知世界的窗口&#xff0c;但近年来&#xff0c;儿童青少年的视力健康却受到了严重困扰。数据显示&#xff0c;近视问题在儿童群体中呈现出明显的增长趋势&#xff0c;这给他们的学习和生活带来了诸多不便。虽然现代科技的快速发展使得电子产品成为了我们生活中不可…...

软件测试面试题:项目中的MQ是如何测试的?

通常&#xff0c;咱们会从两个方面来考虑&#xff1a;正常情况和异常情况。 首先&#xff0c;咱们得确保消息队列在正常工作时结果正确。比如&#xff0c;消息发送出去的时候&#xff0c;所有的字段都得齐全&#xff0c;接收方收到的消息也得一样。咱们得确保系统能够正确无误…...

Python爬取国家医保平台公开数据

国家医保服务平台数据爬取python爬虫数据爬取医疗公开数据 定点医疗机构查询定点零售药店查询医保机构查询药品分类与代码查询 等等&#xff0c;数据都能爬 接口地址&#xff1a;/ebus/fuwu/api/nthl/api/CommQuery/queryFixedHospital 签名参数&#xff1a;signData {dat…...

B站大课堂-自动化精品视频(个人存档)

基础知识 工业通信协议 Modbus 施耐德研发&#xff0c;有基于以太网的 ModbusTCP 协议和使用 485/232 串口通信的 ModbusRTU/ASCII。 Modbus 协议面世较早、协议简洁高效、商用免费、功能灵活、实现简单&#xff0c;是目前应用最广泛的现场总线协议。 我的笔记里边有一些推荐…...

C++_STL---priority_queue

priority_queue的相关介绍 优先级队列是一种容器适配器&#xff0c;根据严格的排序标准&#xff0c;它的第一个元素总是它所包含的元素中最大(小)的。该容器适配器类似于堆&#xff0c;在堆中可以随时插入元素&#xff0c;并且可以检索最大(小)堆元素(优先级队列中位于顶部的元…...

可移动天线辅助宽带通信的性能分析和优化

可移动天线辅助宽带通信的性能分析和优化 可移动天线 (MA) 已成为一种很有前景的技术&#xff0c;通过在发射器 (Tx) 和/或接收器 (Rx) 处实现天线的本地移动来实现更有利的信道条件&#xff0c;从而增强无线通信性能。 由于现有的MA辅助无线通信研究主要考虑平坦衰落信道中的…...

h5兼容table ,如何实现h5在app内使用h5渲染table表格而且实现横屏预览?

压图地址 横屏div 通过css 实现 transform: rotate(90deg); transformOrigin: 50vw 50vw ; height: 100vw; width: 100vh;<divclass"popup-box":style"{transform: originSet 0 ? rotate(90deg) : ,transformOrigin: originSet 0 ? 50vw 50vw : ,height…...

在windows上安装objection

安装命令pip install objection -i https://mirrors.aliyun.com/pypi/simple hook指定进程 objection -g 测试 explore 进程名不定是包名&#xff0c;也可能是app名字&#xff0c;如“测试”就是app的名字 若出现如下错误&#xff0c;说明python 缺少setuptools 直接安装setu…...

人脸特征68点识别 C++

1、加载一张图片 main函数&#xff1a; cv::Mat img cv::imread("5.jpg");vector<Point2f> points_vec dectectFace68(img);2、人脸68特征识别函数 在这里vector<Point2f> dectectFace68(Mat src) {vector<Point2f> points_vec;int* pResults …...

部署LVS-DR 群集

1 LVS-DR 集群 LVS-DR &#xff08;Linux Virtual Server Director Server ) 工作模式&#xff0c; 是生产环境中最常用的一种工作模式 1.1&#xff1a;LVS-DR工作原理 LVS-DR 模式&#xff0c; Director Server 作为群集的访问入口&#xff0c; 不作为网关使用&#xff0c;…...

nginx的正向代理和反向代理

概念 正向代理以及缓存配置 代理&#xff1a;客户端不再是直接访问服务端&#xff0c;通过代理服务器访问服务端。 正向代理&#xff1a;面向客户端&#xff0c;我们通过代理服务器的ip地址访问目标服务器。 服务端只知道代理服务器的地址&#xff0c;真正的客户端ip可以隐…...

米国政府呼吁抛弃 C 和 C++

在开始前刚好我有一些资料&#xff0c;是我根据网友给的问题精心整理了一份「C的资料从专业入门到高级教程」&#xff0c; 点个关注在评论区回复“888”之后私信回复“888”&#xff0c;全部无偿共享给大家&#xff01;&#xff01;&#xff01; 很多观点认为C 或 C永远不可被…...

failed to lazily initialize a collection of role,解决Hibernate查询报错

Hibernate报错&#xff1a; org.hibernate.LazyInitializationException: failed to lazily initialize a collection of role: com.jiuqi.gov.common.attatchment.entity.AttachmentEntity.properties, could not initialize proxy - no Session at org.hibernate.co…...

Promethuse-监控 Etcd

一、思路 Prometheus监控Etcd集群&#xff0c;是没有对应的exporter&#xff0c;而 由CoreOS公司开发的Operator&#xff0c;用来扩展 Kubernetes API&#xff0c;特定的应用程序控制器&#xff0c;它用来创建、配置和管理复杂的有状态应用&#xff0c;如数据库、缓存和监控系…...

linux桌面运维---第四天

1、hostnamectl命令&#xff1a; 作用&#xff1a;永久设置主机名 ​语法&#xff1a;hostnamectl [actions][name strings] ​动作&#xff1a; status 显示当前主机名设置【掌握】 set-hostname NAME 设置系统主机名【掌握】 set-icon-name NAME 为主…...