每个线程一个类实例,C++11

One instance of class per thread, C++11

本文关键字:实例 C++11 一个 线程      更新时间:2023-10-16

我正在为一个多线程应用程序编写一个日志实用程序,我希望能够以类似std::cout的方式调用它:

线程1:

Logger::log << "First message" << Logger::end;

线程2:

Logger::log << "Second message" << Logger::end;

一旦Logger::end被传递到日志,消息应该被刷新到文件/屏幕/网络/日志的任何位置。为了处理对日志的并发写入而不混淆消息,我的想法是每个线程有一个Logger::log实例,然后这些实例与一个工作线程共享对线程安全队列的访问权限,该工作线程专门用于弹出新消息并将其写入文件/屏幕等。

我想实现这一点的一种方法是让某种多单例返回一个实例,这取决于调用它的线程id(可能是从线程id映射到存储在std::map中的日志)。有没有更好和/或更有效的方法?

有没有其他设计不需要每个线程一个日志实例,而我忽略了这一点?std::cout如何处理并发访问?

谢谢!

您可以使用thread_local singletons:

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

编辑:
我重新审视了我的答案,虽然它展示了一般的想法,但具体的例子有一些注意事项:

  1. 虽然std::cout在默认情况下是线程安全的,但对operator<<的多个并行调用中的各个字符仍然允许交错。据我所知,这至少不会发生在Ubuntu上的gcc和clang中,但要真正实现可移植性,您可能必须保护对std::cout或日志系统使用的任何访问
  2. 您必须确保没有人将对Logger实例的引用传递给另一个线程。我不知道为什么要这样做,但这对其他用户来说可能是一个令人惊讶的限制,因为"正常"单身人士不是这样。因此,最好将缓冲区变量stream设为thread_local,而不是记录器

我会跳过多单例。

有一个全局log。CCD_ 9生成一个中间日志对象。intermediate log object << whatever将中间日志对象的内部状态与其返回值共享。

当共享的内部状态最终被销毁时,它会自动将其写入注销。

因此(源代码的)每一行都是原子地发送出去的。

如果要进行多行日志记录,则必须使用auto&& l = log << whatever,然后在需要追加时使用l << more stuff。当l对象被销毁时,它被发送到日志输出。

工业质量:

logintermediate_log上,<< X应该与ostream << X一样工作。

intermediate_log应该存储一个带有magic deleter的std::shared_ptr<std::stringstream>,并将其输入转发到该deleter,然后返回其自身的副本(该副本允许使用auto语法延长生存期)。

magic deleter应该将stringstream内容写入实际输出日志,可能通过异步队列或其他方式(如果存在大量争用,并且写入速度较慢)。

根据直接的问题,最好合理的方法是将记录器(如thread_local变量)存储在singleton中。

但总的来说,我强烈反对你的想法。您应该直接从消息源线程将日志刷新到文件。否则,当程序在消息发送到队列和实际写入光盘之间崩溃时,您可能会丢失一些消息。

我可能在这里建议简单的解决方案,但我使用具有线程安全性的常规单例类:

class Logger{
   private:
     std::mutex mtx;
     ...
   public:
   void log (const std::string& logToPrint){
      std::lock_guard<std::mutex>(mtx);
      //do any logging here..
   } 
};