如何在C++中重载<<线程安全日志记录的运算符?



我正在写一个简单的线程安全日志记录库的草图,我想到了一些事情。代码如下:

#ifndef SimpleLogger_H
#define SimpleLogger_H
#include <iostream>
#include <mutex>
class SimpleLogger
{
public:
template <typename T>
static void log(T message)
{
mutex.lock();
std::cout << message;
mutex.unlock();
}
private:
static std::mutex mutex;
}LOG;
template <typename T>
SimpleLogger &operator<<(SimpleLogger &simpleLogger, T message)
{
simpleLogger.log(message);
return simpleLogger;
}
#endif //SimpleLogger_H

我的想法是像这样使用它:

LOG << "hello" << " world " << 8 << " I can mix integers and strings";

我知道上面的行如下所示:

auto a1 = LOG.operator<<("hello");
auto a2 = a1.operator<<(" world ");
//Another thread may want to use LOG here, and would print in the middle of my message
auto a3 = a2.operator<<(8);
auto a4 = a3.operator<<(" I can mix integers and strings");

如您所见,由于<<被分解为几个函数调用,因此线程可能会在我的消息中间使用LOG对象(我认为消息是一行<<的整个级联(

另外,有没有办法为最后一次<<呼叫自动添加std::endl?我想不出一种方法来做到这一点,但我看到一些日志记录库具有此功能

如何解决这两个问题?

我知道最好使用日志记录库,但我想在一个简单的库中混合 android、桌面和 ios 日志记录,而无需高性能,而且我也对如何克服我在编写自己的时遇到的困难感到困惑

正如其他人已经提到的,在发送到日志文件之前,您需要一个本地缓冲区来收集消息。在下面的示例中,SimpleLoggerBuffer 对象被设计为仅用作临时变量。即它在表达式结束时被破坏。析构函数将缓冲区刷新到日志中,这样您就不必显式调用刷新函数(如果您愿意,也可以在那里添加 endl(

#include <iostream>
#include <sstream>
#include <mutex>
using namespace std;
class SimpleLogger
{
public:    
template <typename T>
static void log(T& message)
{
mutex.lock();
std::cout << message.str();
message.flush();
mutex.unlock();
}
private:
static std::mutex mutex;
}LOG;
std::mutex SimpleLogger::mutex;
struct SimpleLoggerBuffer{
stringstream ss;
SimpleLoggerBuffer() = default;
SimpleLoggerBuffer(const SimpleLoggerBuffer&) = delete;
SimpleLoggerBuffer& operator=(const SimpleLoggerBuffer&) = delete;
SimpleLoggerBuffer& operator=(SimpleLoggerBuffer&&) = delete;
SimpleLoggerBuffer(SimpleLoggerBuffer&& buf): ss(move(buf.ss)) {
}
template <typename T>
SimpleLoggerBuffer& operator<<(T&& message)
{
ss << std::forward<T>(message);
return *this;
}
~SimpleLoggerBuffer() {
LOG.log(ss);
}
};
template <typename T>
SimpleLoggerBuffer operator<<(SimpleLogger &simpleLogger, T&& message)
{
SimpleLoggerBuffer buf;
buf.ss << std::forward<T>(message);
return buf;
}
int main() {
LOG << "hello" << " world " << 8 << " I can mix integers and strings";
}

您可以创建一个帮助程序类,该类收集所有输出并在销毁时打印。大纲:

#include <string>
#include <iostream>
struct Msg;
struct Log {
void print(const Msg &m);
};
struct Msg {
std::string m;
Log &l;
Msg(Log &l) : l(l) {}
~Msg() {
// Print the message on destruction
l.print(*this);
}
};
void Log::print(const Msg &m) {
// Logger specific printing... here, append newline
std::cout << m.m << std::endl;
}
Msg &&operator << (Msg &&m, const std::string &s) {
// Append operator
m.m += s;
return std::move(m);
}

// Helper to log on a specific logger. Just creates the initial message
Msg log(Log &l) { return Msg(l); }
int main()
{
Log l;
log(l) << "a" << "b" << "c";
return 0;
}

由于消息是本地的,因此其他线程不会干扰它。任何必要的锁定都可以在Log.print方法中完成,该方法将接收完整的消息

一个简单的解决方案是写入文件而不是标准输出,特别是每个线程的单独文件。这样就不需要锁定或任何其他同步。如果行具有可解析的格式,则可以稍后合并这些文件。

另一种解决方案是从单个线程异步写入日志,并最初将消息存储在线程安全(可能无锁(队列中。

另外,有没有办法为最后<<呼叫自动添加 std::endl?

除非我误解,否则你可以简单地做stream << message << std::endl.

我认为你可以简单地使用std::clog。它是线程安全的,与 std::cout 相反,旨在立即输出以进行日志记录。 从参考页面:

除非已发出sync_with_stdio(假(,否则可以安全地 同时从多个线程访问这些对象 格式化和未格式化输出。

我向你推荐这个杰森·特纳关于cout,堵塞和错误的视频。

最简单的方法是从第一个<<返回一个临时代理 - 这可以记录您的流(并在销毁时解锁(,或者只是构建一个本地 ostringstream 并在单个调用中刷新它(再次,在销毁时(。

在日志记录时保持锁定对性能不是很好(尽管它比现有的日志方法更好,后者应使用std::lock_guard以确保异常安全(。

构建和丢弃临时 ostringstream 可能更好,但如果你关心性能,则需要进行基准测试,并且最终可能需要更复杂的东西(每线程循环缓冲区、mmap 文件或其他东西(。

相关内容

最新更新