【问题标题】:multiple threads writing to std::cout or std::cerr多个线程写入 std::cout 或 std::cerr
【发布时间】:2013-02-08 15:22:06
【问题描述】:

我有通过 cout 和 cerr 写入控制台的 OpenMP 线程。这当然是不安全的,因为输出可以交错。我可以做类似的事情

#pragma omp critical(cerr)
{
   cerr << "my variable: " << variable << endl;
}

如果可以用线程安全版本替换 cerr 会更好,类似于 valgrind DRD 手册 (http://valgrind.org/docs/manual/drd-manual.html#drd-manual.effective-use) 中解释的方法,该方法涉及从 std::ostreambuf 派生一个类。理想情况下,最后我会用我自己的线程 cerr 替换 cerr,例如简单地说:

tcerr << "my variable: " << variable << endl;

这样的类可以在遇到“endl”时立即打印到控制台。我不介意来自不同线程的行是否交错,但每一行应该只来自一个线程。

我真的不明白 C++ 中的所有这些流是如何工作的,它太复杂了。有没有人有这样的课程,或者可以告诉我如何为此目的创建这样的课程?

【问题讨论】:

  • 请不要建议 printf.. ;)
  • “这当然不安全” - 这在 C++11 中是不正确的,除非你有意识地采取行动使其成为现实。
  • 你的标题是 cout 而不是 cerr
  • @Wolfgang:printfwrite 有什么问题?在字符串流中构建字符串,然后使用printf/write 对整行进行原子写入...
  • @AndyProwl:即使在 C++11 中,上面的代码也涉及到对operator&lt;&lt; 的多次调用,这意味着不同线程的输出可以混合产生:myvariable: myvariable: 345(现在看看是否这些值为345345 :))

标签: c++ multithreading openmp valgrind


【解决方案1】:

正如其他人指出的那样,在 C++11 中,std::cout 线程安全的。

但是如果你像这样使用它

std::cout << 1 << 2 << 3;

对于不同的线程,输出仍然可以交错,因为每个&lt;&lt; 都是一个新的函数调用,可以在另一个线程上的任何函数调用之前。

为了避免交错没有 #pragma omp critical - 这将锁定所有内容 - 您可以执行以下操作:

std::stringstream stream; // #include <sstream> for this
stream << 1 << 2 << 3;
std::cout << stream.str();

将 123 写入流的三个调用仅发生在一个本地非共享对象的线程中,因此不受任何其他线程的影响。然后,只有一次调用共享输出流std::cout,其中项目123的顺序已经固定,因此不会弄乱。

【讨论】:

  • 您是否碰巧只有 operatorstackoverflow.com/questions/6374264/… 表明它是“安全的”,即不会引起竞争,但仍可能导致交错。
【解决方案2】:

我没有足够的声誉来发表评论,但我想发表我对 AtomicWriter 类的添加以支持 std::endl 并允许使用除 std::cout 之外的其他流。这里是:

class AtomicWriter {
    std::ostringstream st;
    std::ostream &stream;
public:
    AtomicWriter(std::ostream &s=std::cout):stream(s) { }
    template <typename T>
    AtomicWriter& operator<<(T const& t) {
        st << t;
        return *this;
    }
    AtomicWriter& operator<<( std::ostream&(*f)(std::ostream&) ) {
        st << f;
        return *this;
    }
    ~AtomicWriter() { stream << st.str(); }
};

【讨论】:

    【解决方案3】:

    您可以使用类似于字符串生成器的方法。创建一个非模板类:

    • 提供模板化 operator&lt;&lt; 以插入此对象
    • 内部构建为std::ostringstream
    • 在销毁时转储内容

    粗略的方法:

     class AtomicWriter {
        std::ostringstream st;
     public:
        template <typename T> 
        AtomicWriter& operator<<(T const& t) {
           st << t;
           return *this;
        }
        ~AtomicWriter() {
           std::string s = st.str();
           std::cerr << s;
           //fprintf(stderr,"%s", s.c_str());
           // write(2,s.c_str(),s.size());
        }
     };
    

    用作:

    AtomicWriter() << "my variable: " << variable << "\n";
    

    或者在更复杂的场景中:

    {
       AtomicWriter w;
       w << "my variables:";
       for (auto & v : vars) {
          w << ' ' << v;
       }
    }  // now it dumps
    

    如果你想要操纵器,你需要添加更多的重载,你可以使用write而不是fprintf作为析构函数中的原子写入,或者std::cerr,你可以这样概括将目标传递给构造函数(std::ostream/文件描述符/FILE*),

    【讨论】:

    • 我想我还会添加一个 flush 成员,它与析构函数一样并清除内部缓冲区。然后,如果您愿意,您可以一遍又一遍地重用相同的原子。有些人可能更喜欢使用第二个示例中的额外范围。
    • @MooingDuck:不知道该怎么走...我明白你的要求,但我发现范围允许我在查看逻辑而不是跟踪时忽略内容(我们的日志框架允许类似的结构)。也就是说,当正确使用时(即不要将逻辑与日志混合),范围可以用于分析内容并确保不存在真正的逻辑,之后我不需要尝试解释内部循环是什么如果我正在查看整个函数的逻辑,正在做。
    • IMO 这在析构函数中隐藏了太多有趣的逻辑;使消费代码难以理解;正如评论指出的那样,有趣的事情发生在哪里。添加刷新似乎是一个好主意,但它有两种模式:显式(刷新)和隐式,这很令人困惑。添加更多的重载听起来像是在重新发明轮子。暴露字符串流;或者只是使用它而不是定义一个基本上做同样事情的新类。
    【解决方案4】:

    您可以通过继承std::basic_streambuf 来做到这一点,并重写正确的函数以使其成为线程安全的。然后将此类用于您的流对象。

    【讨论】:

      猜你喜欢
      • 2015-08-16
      • 2020-10-02
      • 2014-07-02
      • 2013-08-19
      • 2012-03-09
      • 2017-04-30
      • 1970-01-01
      • 2010-09-26
      • 2022-01-05
      相关资源
      最近更新 更多