多个线程写入 std::cout 或 std::cerr

20

我有使用OpenMP线程通过cout和cerr写入控制台的代码。这显然是不安全的,因为输出可能会交错。我可以采取以下措施:

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

如果能使用类似于valgrind DRD手册中所解释的方法(http://valgrind.org/docs/manual/drd-manual.html#drd-manual.effective-use),从std :: ostreambuf派生一个类,以替换cerr为线程安全版本,那就更好了。最理想的情况是最终我只需将cerr替换为自己的线程化cerr,例如:

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

这样的类可以在遇到“endl”时立即打印到控制台。我不介意来自不同线程的行交错,但每行只能来自一个线程。
我真的不理解C ++中所有这种流式处理是如何工作的,它太复杂了。有没有人有这样的类或可以向我展示如何为此创建这样的类?

请不要建议printf.. ;) - Wolfgang
是的,也许我没有正确使用“线程安全”的术语... - Wolfgang
1
@Wolfgang:printfwrite有什么问题吗?在stringstream中构建字符串,然后使用printf/write对整行进行原子写入... - David Rodríguez - dribeas
2
@AndyProwl:即使在C++11中,上面的代码仍涉及多次调用operator<<,这意味着不同线程的输出可能会混合产生:myvariable: myvariable: 345(现在请想象一下这些值是345还是345 :)) - David Rodríguez - dribeas
@DavidRodríguez-dribeas:是的,我知道。我只是想纠正关于线程安全的说法。 - Andy Prowl
显示剩余5条评论
5个回答

46

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

然而,如果你像这样使用它

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

使用不同的线程,输出仍然可以交错进行,因为每个<<都是一个新的函数调用,可以在另一个线程上的任何函数调用之前调用。
为了避免交错而没有#pragma omp critical(这将锁定所有内容),您可以执行以下操作:
std::stringstream stream; // #include <sstream> for this
stream << 1 << 2 << 3;
std::cout << stream.str();

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

你是否有只调用operator<<而不是单个字符交错的来源?https://dev59.com/SWw15IYBdhLWcg3w0fKh表明它在不引起竞争的意义上是“安全”的,但仍可能导致交错。 - Cactus Golov

14

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

  • 提供用于插入到此对象中的模板化operator<<
  • 内部构建为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*),从而实现泛化。请注意保留HTML标签。

我认为我还会添加一个flush成员,它与析构函数相同并清除内部缓冲区。然后,如果您希望重复使用同一原子,则可以这样做。有些人可能更喜欢这种方法,而不是像您第二个示例中那样使用额外的作用域。 - Mooing Duck
@MooingDuck:不确定该怎么做...我理解你的要求,但我发现当我查看逻辑而非跟踪时,范围允许我忽略内容(我们的日志框架允许类似的结构)。也就是说,当使用正确时(即不将逻辑与日志记录混合),可以使用范围分析内容并确保没有真正的逻辑存在,此后如果我正在查看整个函数的逻辑,则无需尝试解释内部循环在做什么。 - David Rodríguez - dribeas
在我的看法中,析构函数隐藏了太多有趣的逻辑,这使得消费代码难以理解,正如注释所指示的那样,有趣的事情发生在其中。添加flush似乎是个好主意,但它会有两种模式:显式(flush)和隐式,这很令人困惑。而且添加更多重载似乎就像重新发明轮子一样。暴露stringstream;或者只是使用它来代替定义一个基本相同的新类。 - steve

7

我没有足够的声望来发布评论,但我想要在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(); }
};

1

将以下代码放在头文件atomic_stream_macro.h中:

#ifndef atomic_stream_macro_h
#define atomic_stream_macro_h

#include <mutex>

/************************************************************************/
/************************************************************************/

extern std::mutex print_mutex;

#define PRINT_MSG(out,msg)                                           \
{                                                                    \
    std::unique_lock<std::mutex> lock (print_mutex);                 \
                                                                     \
    out << __FILE__ << "(" << __LINE__ << ")" << ": "                \
        << msg << std::endl;                                         \
}

/************************************************************************/
/************************************************************************/

#endif

现在,可以按照以下方式从文件中使用宏。
#include <atomic_stream_macro.h>
#include <iostream>

int foo (void)
{
    PRINT_MSG (std::cout, "Some " << "Text " << "Here ");
}

最后,在 main.cxx 中声明互斥锁。

#include <mutex>

std::mutex print_mutex;

int main (void)
{
    // launch threads from here
}

0
你可以通过继承std::basic_streambuf,并重写正确的函数使其线程安全。然后将此类用于您的流对象。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接