TensorRT笔记(2):解析样例中Logger日志类的设计
在https://blog.csdn.net/ouliten/article/details/154490047?spm=1001.2014.3001.5502#t5
我把Logger这个类给贴出来了,但是关键的实现机制并没有详细讲解
这个Logger类继承了nvinfer1::ILogger,最关键的重写log方法实现如下
void log(Severity severity, const char* msg) noexcept override{LogStreamConsumer(mReportableSeverity, severity) << "[TRT] " << std::string(msg) << std::endl;}
很明显这个log方法委托给了LogStreamConsumer这个类取实现。而且LogStreamConsumer这个类样子上就很明显像std::ostream的使用方法。
下面就从头开始分析这个类的实现
LogStreamConsumerBuffer
using Severity=nvinfer1::ILogger::Severity;
class LogStreamConsumerBuffer:public std::stringbuf{
public:LogStreamConsumerBuffer(std::ostream& stream, const std::string& prefix, bool shouldLog): mOutput(stream), mPrefix(prefix), mShouldLog(shouldLog){}LogStreamConsumerBuffer(LogStreamConsumerBuffer&& other) noexcept: mOutput(other.mOutput), mPrefix(other.mPrefix), mShouldLog(other.mShouldLog){}LogStreamConsumerBuffer(const LogStreamConsumerBuffer& other) = delete;LogStreamConsumerBuffer() = delete;LogStreamConsumerBuffer& operator=(const LogStreamConsumerBuffer&) = delete;LogStreamConsumerBuffer& operator=(LogStreamConsumerBuffer&&) = delete;~LogStreamConsumerBuffer() override{// std::streambuf::pbase() gives a pointer to the beginning of the buffered part of the output sequence// std::streambuf::pptr() gives a pointer to the current position of the output sequence// if the pointer to the beginning is not equal to the pointer to the current position,// call putOutput() to log the output to the streamif (pbase() != pptr()){putOutput();}}//!//! synchronizes the stream buffer and returns 0 on success//! synchronizing the stream buffer consists of inserting the buffer contents into the stream,//! resetting the buffer and flushing the stream//!//C++ 流机制在每次 flush 或 std::endl 时会调用int32_t sync() override{putOutput();return 0;}//将缓存内容输出到 mOutput。//自动添加时间戳和前缀。//清空缓冲区并 flush。void putOutput(){if(mShouldLog){std::time_t timestamp=std::time(nullptr);tm *tm_local=std::localtime(×tamp);mOutput << "[";mOutput << std::setw(2) << std::setfill('0') << 1 + tm_local->tm_mon << "/";mOutput << std::setw(2) << std::setfill('0') << tm_local->tm_mday << "/";mOutput << std::setw(4) << std::setfill('0') << 1900 + tm_local->tm_year << "-";mOutput << std::setw(2) << std::setfill('0') << tm_local->tm_hour << ":";mOutput << std::setw(2) << std::setfill('0') << tm_local->tm_min << ":";mOutput << std::setw(2) << std::setfill('0') << tm_local->tm_sec << "] ";// std::stringbuf::str() gets the string contents of the buffer// insert the buffer contents pre-appended by the appropriate prefix into the streammOutput << mPrefix << str();}// set the buffer to emptystr("");// flush the streammOutput.flush();}void setShouldLog(bool shouldLog){mShouldLog = shouldLog;}
private:std::ostream &mOutput;//真正输出日志的流std::string mPrefix;//日志前缀,比如 [E] 或 [I]bool mShouldLog{};//是否实际输出日志,用于根据 severity 筛选日志
};
这个类对std::ostream做了一个包装。实现了具体的输出的功能
LogStreamConsumerBase
//提供线程安全的日志缓冲
class LogStreamConsumerBase
{
public:LogStreamConsumerBase(std::ostream& stream, const std::string& prefix, bool shouldLog): mBuffer(stream, prefix, shouldLog){}protected:std::mutex mLogMutex;LogStreamConsumerBuffer mBuffer;
}; // class LogStreamConsumerBase
这个类比较简单,为LogStreamConsumerBuffer提供了一个锁。目的是防止多线程同时调用log,导致输出混乱。
LogStreamConsumer
//继承顺序很重要:先 LogStreamConsumerBase 初始化 mBuffer,再把 mBuffer 的地址传给 std::ostream 构造函数。
class LogStreamConsumer:protected LogStreamConsumerBase,public std::ostream{
public:LogStreamConsumer(nvinfer1::ILogger::Severity reportableSeverity,nvinfer1::ILogger::Severity severity):LogStreamConsumerBase(severityOstream(severity),severityPrefix(severity),severity<=reportableSeverity),std::ostream(&mBuffer),mShouldLog(severity<=reportableSeverity),//根据 Severity 决定是否输出日志mSeverity(severity){}LogStreamConsumer(LogStreamConsumer&& other) noexcept: LogStreamConsumerBase(severityOstream(other.mSeverity), severityPrefix(other.mSeverity), other.mShouldLog), std::ostream(&mBuffer) // links the stream buffer with the stream, mShouldLog(other.mShouldLog), mSeverity(other.mSeverity){}LogStreamConsumer(const LogStreamConsumer& other) = delete;LogStreamConsumer() = delete;~LogStreamConsumer() override = default;LogStreamConsumer& operator=(const LogStreamConsumer&) = delete;LogStreamConsumer& operator=(LogStreamConsumer&&) = delete;void setReportableSeverity(Severity reportableSeverity){mShouldLog = mSeverity <= reportableSeverity;mBuffer.setShouldLog(mShouldLog);}std::mutex& getMutex(){return mLogMutex;}bool getShouldLog() const{return mShouldLog;}
private:static std::ostream& severityOstream(Severity severity){//自动选择输出流:return severity>=Severity::kINFO?std::cout:std::cerr;}static std::string severityPrefix(Severity severity){switch (severity){case Severity::kINTERNAL_ERROR: return "[F] ";case Severity::kERROR: return "[E] ";case Severity::kWARNING: return "[W] ";case Severity::kINFO: return "[I] ";case Severity::kVERBOSE: return "[V] ";default: assert(0); return "";}}bool mShouldLog;Severity mSeverity;
};// class LogStreamConsumer
重载<<方法
//模板实现,可以像普通 ostream 一样使用:
template <typename T>
LogStreamConsumer& operator<<(LogStreamConsumer& logger, const T& obj)
{if (logger.getShouldLog()){std::lock_guard<std::mutex> guard(logger.getMutex());//使用互斥锁保证多线程安全。auto& os = static_cast<std::ostream&>(logger);os << obj;}return logger;
}
//!
//! Special handling std::endl
//!
inline LogStreamConsumer& operator<<(LogStreamConsumer& logger, std::ostream& (*f)(std::ostream&) )
{if (logger.getShouldLog()){std::lock_guard<std::mutex> guard(logger.getMutex());auto& os = static_cast<std::ostream&>(logger);os << f;}return logger;
}
inline LogStreamConsumer& operator<<(LogStreamConsumer& logger, const nvinfer1::Dims& dims)
{if (logger.getShouldLog()){std::lock_guard<std::mutex> guard(logger.getMutex());auto& os = static_cast<std::ostream&>(logger);for (int32_t i = 0; i < dims.nbDims; ++i){os << (i ? "x" : "") << dims.d[i];}}return logger;
}
找到关键点,在具体实现的时候
auto& os = static_cast<std::ostream&>(logger);
os << obj;
我们将LogStreamConsumer& logger强制转换成std::ostream&类型,用来模拟<<操作
而logger的std::ostream父类,绑定了&mBuffer。
mBuffer是LogStreamConsumerBuffer类,继承自std::stringbuf
std::ostream 有一个构造函数:
explicit ostream(streambuf* sb);
-
也就是说,
std::ostream可以接受一个 指向std::streambuf的指针 来初始化输出缓冲区。 -
所有写入
ostream的操作(<<、write()、flush()等)都会最终调用streambuf的 虚函数:-
overflow()→ 当缓冲区满时写入。 -
xsputn()→ 写入连续字符。 -
sync()→ flush 同步缓冲区。
-
当 flush 或 std::endl 触发:
mBuffer.sync()
-
调用
LogStreamConsumerBuffer::sync(),内部调用putOutput()。 -
putOutput()会:-
生成时间戳
-
添加前缀([I]、[E] 等)
-
将缓冲区的字符串写入
mOutput(也就是std::cout/std::cerr) -
清空
stringbuf并 flushmOutput
-
核心设计理念
-
继承 + 委托
-
LogStreamConsumer继承std::ostream提供流式接口 -
输出实现委托给
mBuffer,分离接口与逻辑
-
-
组合 + 初始化顺序控制
-
mBuffer在LogStreamConsumerBase中初始化,保证构造顺序正确 -
std::ostream构造时拿到已初始化的缓冲区
-
-
最终输出到指定流
-
mBuffer内部的mOutput决定真正输出的设备 -
允许根据 severity 动态选择
std::cout或std::cerr
-
LogStreamConsumerBuffer设计解析
它继承 std::stringbuf,又持有一个 std::ostream&,看起来像“自己写自己还要交给别人写”,其实逻辑如下:
① 继承 std::stringbuf
-
目的:利用
stringbuf的缓冲功能。-
stringbuf内部有一个可写字符串缓冲区。 -
当你做
sputn()或operator<<写入时,数据先写到这个缓冲区。 -
你可以重载
sync(),在 flush 的时候把缓冲区的内容输出到外部。
-
-
也就是:
stringbuf用作缓存层。
② 内部维护 std::ostream& mOutput
-
目的:决定最终输出流去向。
-
日志可能要输出到
std::cout、std::cerr或文件流。 -
mOutput保存这个目标。
-
-
当缓冲区 flush(调用
sync()或析构)时:-
取出缓冲区内容
str() -
添加时间戳、前缀
-
写入
mOutput -
flush
mOutput
-
感觉怪怪的?
LogStreamConsumerBuffer 既继承了 std::stringbuf(给 std::ostream 提供缓冲区),又自己内部持有一个 std::ostream& mOutput。表面上看好像重复了,其实逻辑上是合理的,而且在 C++ 日志系统里是一个典型模式。
-
std::ostream本身不存储数据,它只是一个流接口。 -
std::ostream需要一个std::streambuf*来实际存储和输出数据: -
当你写入
os << "msg": -
数据写入
buffer(调用sputn()或overflow()) -
flush/析构时,调用
buffer.sync()
发现了吗,如果是你自己实现的std::ostream子类对象,调用<<,数据只是放在了buffer里,并没有输出。
那究竟谁能输出呢?答案是std::cout/std::cerr.
LogStreamConsumerBuffer 内部有:
std::ostream& mOutput; // 最终输出目标
它不是用来缓冲的,而是决定最终输出流(例如 std::cout 或 std::cerr)。
LogStreamConsumerBuffer 设计了 中间缓存 + 格式化 + 最终输出:
-
继承
stringbuf:拦截 ostream 写入,允许缓存和格式化 -
内部
std::ostream& mOutput:决定写到哪里 -
如果内部没有
std::ostream& mOutput,那么你依然要在其他地方创建一个std::ostream&然后输出stringbuf的内容
为什么LogStreamConsumer要继承LogStreamConsumerBase
其实在逻辑上看,LogStreamConsumer与LogStreamConsumerBase没那么像父子同类的关系。所以如果把这两改成组合关系,也不是不可以?
不过因为std::ostream的父类要求stringbuf指针。通过按序继承的方式,能保证父类初始化的顺序。
