每个线程一个 class 实例,C++11
One instance of class per thread, C++11
我正在为多线程应用程序编写一个日志实用程序,我希望能够以类似 std::cout 的方式调用它:
线程 1:
Logger::log << "First message" << Logger::end;
线程 2:
Logger::log << "Second message" << Logger::end;
一旦 Logger::end
传递到日志,该消息应刷新到 file/screen/network/whatever 日志。为了在不混淆消息的情况下处理对日志的并发写入,我的想法是每个线程都有一个 Logger::log
实例,然后这些实例与一个专门用于弹出新消息并写入它们的工作线程共享对线程安全队列的访问到 file/screen 等等
我想实现的一种方法是让某种多单例返回一个实例,具体取决于调用它的线程 ID(从线程 ID 映射到存储在 std::map 中的日志) .有没有更好的and/or更有效的方法?
有没有我忽略的不需要每个线程一个日志实例的其他设计? std::cout 如何处理并发访问?
谢谢!
我会跳过那个多单例。
拥有全球log
。 log << whatever
生成一个中间日志对象。 intermediate log object << whatever
与其 return 值共享中间日志对象的内部状态。
当共享内部状态最终被销毁时,它自动将其写入注销。
因此(源代码的)每一行都自动发送出去。
如果你想做多行记录,你必须使用auto&& l = log << whatever
,然后当你想追加时使用l << more stuff
。当 l
对象被销毁时,它被发送到日志输出。
工业品质:
在 log
和 intermediate_log
上,如果 ostream << X
有效,<< X
应该有效。
intermediate_log
应该存储一个 std::shared_ptr<std::stringstream>
和一个魔法删除器,它将输入转发到它,然后 return 自己的一个副本(该副本允许生命周期延长 auto
语法)。
magic deleter 应该将 stringstream
内容写入实际输出日志,可能通过异步队列或其他方式(如果有很多争用,写入速度很慢)。
根据直接问题,最合理的方法是在单例中存储一个像 thread_local
变量这样的记录器。
但总的来说我强烈反对你的想法。您应该直接从消息源线程 将日志刷新到文件。否则,当您的程序在消息发送到队列和实际写入磁盘之间崩溃时,您可能会丢失一些消息。
您可以使用 thread_local 单例:
class Logger {
public:
struct Sentinel{};
static thread_local Logger log;
static Sentinel end;
template<class T>
Logger& operator<<(T data) {
stream << data;
return *this;
}
//for endl and so on
Logger& operator<<(std::ostream& (*pf)(std::ostream&)) {
pf(stream);
return *this;
}
private:
Logger(){};
std::stringstream stream;
};
thread_local Logger Logger::log;
Logger::Sentinel Logger::end;
template<>
Logger& Logger::operator<<<Logger::Sentinel>(Logger::Sentinel data) {
stream << std::endl;
std::cout << stream.str();
stream.str("");
return *this;
}
另一种可能的语法:
class Logger_t {
public:
template<class T>
Logger_t& operator<<(T data) {
stream << data;
return *this;
}
//for endl and other stream manipulators
Logger_t& operator<<(std::ostream& (*pf)(std::ostream&)) {
pf(stream);
return *this;
}
void flush() {
std::cout << stream.str();
stream.str("");
}
private:
Logger_t(){};
std::stringstream stream;
friend Logger_t& Logger();
};
Logger_t& Logger() {
thread_local Logger_t logger;
return logger;
}
用法:
int main() {
Logger() << "test1 " << "test2" << std::endl;
Logger() << "test3" << std::endl;
Logger().flush();
Logger() << "test4" << std::endl; // <-- Not flushed
}
输出:
test1 test2
test3
编辑:
我重新审视了我的答案,虽然它展示了一般想法,但具体示例有一些注意事项:
- 虽然
std::cout
默认情况下是线程安全的,但来自对 operator<<
的多个并行调用的单个字符仍然允许交错。据我所知,这至少不会发生在 Ubuntu 上的 gcc 和 clang 上,但要真正便携,您可能必须保护对 std::cout
或您的日志系统使用的任何内容的任何访问。
- 您必须确保没有人将对 Logger 实例的引用传递给另一个线程。我不知道为什么要这样做,但这对其他用户来说可能是一个令人惊讶的限制,因为 "normal" 单身人士并非如此。因此,最好使缓冲区变量
stream
thread_local
而不是记录器。
我可能会在这里建议简单的解决方案,但我会使用具有线程安全性的常规单例 class :
class Logger{
private:
std::mutex mtx;
...
public:
void log (const std::string& logToPrint){
std::lock_guard<std::mutex>(mtx);
//do any logging here..
}
};
我正在为多线程应用程序编写一个日志实用程序,我希望能够以类似 std::cout 的方式调用它:
线程 1:
Logger::log << "First message" << Logger::end;
线程 2:
Logger::log << "Second message" << Logger::end;
一旦 Logger::end
传递到日志,该消息应刷新到 file/screen/network/whatever 日志。为了在不混淆消息的情况下处理对日志的并发写入,我的想法是每个线程都有一个 Logger::log
实例,然后这些实例与一个专门用于弹出新消息并写入它们的工作线程共享对线程安全队列的访问到 file/screen 等等
我想实现的一种方法是让某种多单例返回一个实例,具体取决于调用它的线程 ID(从线程 ID 映射到存储在 std::map 中的日志) .有没有更好的and/or更有效的方法?
有没有我忽略的不需要每个线程一个日志实例的其他设计? std::cout 如何处理并发访问?
谢谢!
我会跳过那个多单例。
拥有全球log
。 log << whatever
生成一个中间日志对象。 intermediate log object << whatever
与其 return 值共享中间日志对象的内部状态。
当共享内部状态最终被销毁时,它自动将其写入注销。
因此(源代码的)每一行都自动发送出去。
如果你想做多行记录,你必须使用auto&& l = log << whatever
,然后当你想追加时使用l << more stuff
。当 l
对象被销毁时,它被发送到日志输出。
工业品质:
在 log
和 intermediate_log
上,如果 ostream << X
有效,<< X
应该有效。
intermediate_log
应该存储一个 std::shared_ptr<std::stringstream>
和一个魔法删除器,它将输入转发到它,然后 return 自己的一个副本(该副本允许生命周期延长 auto
语法)。
magic deleter 应该将 stringstream
内容写入实际输出日志,可能通过异步队列或其他方式(如果有很多争用,写入速度很慢)。
根据直接问题,最合理的方法是在单例中存储一个像 thread_local
变量这样的记录器。
但总的来说我强烈反对你的想法。您应该直接从消息源线程 将日志刷新到文件。否则,当您的程序在消息发送到队列和实际写入磁盘之间崩溃时,您可能会丢失一些消息。
您可以使用 thread_local 单例:
class Logger {
public:
struct Sentinel{};
static thread_local Logger log;
static Sentinel end;
template<class T>
Logger& operator<<(T data) {
stream << data;
return *this;
}
//for endl and so on
Logger& operator<<(std::ostream& (*pf)(std::ostream&)) {
pf(stream);
return *this;
}
private:
Logger(){};
std::stringstream stream;
};
thread_local Logger Logger::log;
Logger::Sentinel Logger::end;
template<>
Logger& Logger::operator<<<Logger::Sentinel>(Logger::Sentinel data) {
stream << std::endl;
std::cout << stream.str();
stream.str("");
return *this;
}
另一种可能的语法:
class Logger_t {
public:
template<class T>
Logger_t& operator<<(T data) {
stream << data;
return *this;
}
//for endl and other stream manipulators
Logger_t& operator<<(std::ostream& (*pf)(std::ostream&)) {
pf(stream);
return *this;
}
void flush() {
std::cout << stream.str();
stream.str("");
}
private:
Logger_t(){};
std::stringstream stream;
friend Logger_t& Logger();
};
Logger_t& Logger() {
thread_local Logger_t logger;
return logger;
}
用法:
int main() {
Logger() << "test1 " << "test2" << std::endl;
Logger() << "test3" << std::endl;
Logger().flush();
Logger() << "test4" << std::endl; // <-- Not flushed
}
输出:
test1 test2
test3
编辑:
我重新审视了我的答案,虽然它展示了一般想法,但具体示例有一些注意事项:
- 虽然
std::cout
默认情况下是线程安全的,但来自对operator<<
的多个并行调用的单个字符仍然允许交错。据我所知,这至少不会发生在 Ubuntu 上的 gcc 和 clang 上,但要真正便携,您可能必须保护对std::cout
或您的日志系统使用的任何内容的任何访问。 - 您必须确保没有人将对 Logger 实例的引用传递给另一个线程。我不知道为什么要这样做,但这对其他用户来说可能是一个令人惊讶的限制,因为 "normal" 单身人士并非如此。因此,最好使缓冲区变量
stream
thread_local
而不是记录器。
我可能会在这里建议简单的解决方案,但我会使用具有线程安全性的常规单例 class :
class Logger{
private:
std::mutex mtx;
...
public:
void log (const std::string& logToPrint){
std::lock_guard<std::mutex>(mtx);
//do any logging here..
}
};