【问题标题】:iostream thread safety, must cout and cerr be locked separately?iostream 线程安全,必须分别锁定 cout 和 cerr 吗?
【发布时间】:2013-01-16 06:02:22
【问题描述】:

我了解,为避免输出混合,多个线程对 cout 和 cerr 的访问必须同步。在同时使用 cout 和 cerr 的程序中,单独锁定它们是否足够?还是同时写入 cout 和 cerr 仍然不安全?

编辑说明:我知道 cout 和 cerr 在 C++11 中是“线程安全的”。我的问题是,不同线程同时写入 cout 和写入 cerr 是否会像两次写入 cout 那样相互干扰(导致交错输入等)。

【问题讨论】:

  • 它永远不会“不安全”。你可能只是没有得到你所期望的。
  • 我想澄清一下。使用一个全局锁写入 cout 和 cerr 与分别使用单独的锁在行为上是否存在差异?
  • 您可以使用不同的锁。它们不相互依赖。
  • 或许将这个问题表述为“可能 cout 和 cerr 被分别锁定?”

标签: c++ thread-safety cout


【解决方案1】:

如果你执行这个函数:

void f() {
    std::cout << "Hello, " << "world!\n";
}

从多个线程中,您将获得两个字符串"Hello, ""world\n" 或多或少的随机交错。那是因为有两个函数调用,就像你写的代码是这样的:

void f() {
    std::cout << "Hello, ";
    std::cout << "world!\n";
}

为了防止这种交错,你必须添加一个锁:

std::mutex mtx;
void f() {
    std::lock_guard<std::mutex> lock(mtx);
    std::cout << "Hello, " << "world!\n";
}

也就是说,交织问题与cout无关。这是关于使用它的代码:有两个单独的函数调用插入文本,所以除非你阻止多个线程同时执行相同的代码,否则函数调用之间可能会发生线程切换,这就是给你的交错。

请注意,互斥锁不会阻止线程切换。在前面的代码sn-p中,它防止两个线程同时执行f()的内容;其中一个线程必须等到另一个线程完成。

如果你写信给cerr,你会遇到同样的问题,并且你会得到交错输出,除非你确保你永远不会有两个线程在同时,这意味着两个函数必须使用相同的互斥锁:

std::mutex mtx;
void f() {
    std::lock_guard<std::mutex> lock(mtx);
    std::cout << "Hello, " << "world!\n";
}

void g() {
    std::lock_guard<std::mutex> lock(mtx);
    std::cerr << "Hello, " << "world!\n";
}

【讨论】:

  • 应该注意的是,如果操作系统将coutcerr 指向同一个源(比如...控制台),那么文本将被交错,除非它们共享相同互斥体。
  • 操作系统和库与它有很大关系。 Linux 中的线程可以随意输出到标准输出,无需锁定,但单独的文本行不会被另一个线程的标准输出打乱。到 stderr 的输出没有以同样的方式缓冲,所以它会被弄乱。这种行为差异会导致某些版本的 Eclipse CDT 出现问题;编译器错误出现在 stderr 上,并且没有出现在 Eclipse 控制台中编译器的 stdout 的正确位置。
【解决方案2】:

在 C++11 中,与 C++03 不同,全局流对象(coutcincerrclog)的插入和提取是线程安全的。无需提供手动同步。但是,由不同线程插入的字符可能会在输出时发生不可预测的交错;同样,当多个线程从标准输入读取时,无法预测哪个线程将读取哪个令牌。

全局流对象的线程安全默认是激活的,但是可以通过调用流对象的sync_with_stdio成员函数并将false作为参数传递来关闭它。在这种情况下,您将不得不手动处理同步。

【讨论】:

    【解决方案3】:

    同时写入 cout 和 cerr 可能不安全! 这取决于 cout 是否绑定到 cerr。见std::ios::tie

    "绑定流是一个输出流对象,它在之前被刷新 此流对象中的每个 i/o 操作。”

    这意味着,cout.flush() 可能会被写入 cerr 的线程无意中调用。 我花了一些时间弄清楚,这就是我的一个项目中 cout 输出中随机缺少行尾的原因:(

    对于 C++98,cout 不应绑定到 cerr。但是,尽管有标准,但在使用 MSVC 2008(我的经验)时它是绑定的。使用以下代码时,一切正常。

    std::ostream *cerr_tied_to = cerr.tie();
    if (cerr_tied_to) {
        if (cerr_tied_to == &cout) {
            cerr << "DBG: cerr is tied to cout ! -- untying ..." << endl;
            cerr.tie(0);
        }
    }
    

    另见:why cerr flushes the buffer of cout

    【讨论】:

    • 有趣。可以肯定的是,这并不像 UB 那样不安全,它“只是”表现不佳。
    • 哇...这让我发现了一些难以发现的错误。
    【解决方案4】:

    这里已经有几个答案了。我将总结并解决它们之间的交互。

    通常,

    std::coutstd::cerr 通常会合并到一个文本流中,因此将它们锁定在共同的位置会导致最有用的程序。

    如果您忽略该问题,coutcerr 默认别名为它们的 stdio 对应物,它们是线程安全的 as in POSIX,直到标准 I/O 函数 (C++14 §27.4. 1/4,比单独使用 C 的保证更强)。如果您坚持使用这种功能选择,您会得到垃圾 I/O,但不会出现未定义的行为(语言律师可能会将其与“线程安全”联系起来,而不管有用性如何)。

    但是,请注意,虽然标准格式的 I/O 函数(例如读取和写入数字)是线程安全的,但用于更改格式的操纵器(例如用于十六进制的 std::hex 或用于限制输入字符串的 std::setw大小)不是。因此,通常不能假设省略锁是安全的。

    如果选择单独锁定,事情就复杂了。

    单独锁定

    为了提高性能,可以通过分别锁定coutcerr 来减少锁争用。它们是单独缓冲(或非缓冲)的,它们可能会刷新到单独的文件中。

    默认情况下,cerr 在每次操作之前刷新cout,因为它们是“绑定的”。这会破坏分离和锁定,所以记得在使用它之前调用cerr.tie( nullptr )。 (这同样适用于cin,但不适用于clog。)

    stdio解耦

    标准规定coutcerr 上的操作不会引入种族,但这并不是它的确切含义。流对象并不特殊;它们的底层streambuf 缓冲区是。

    此外,调用std::ios_base::sync_with_stdio 旨在删除标准流的特殊方面——允许它们像其他流一样被缓冲。虽然标准没有提到 sync_with_stdio 对数据竞争的任何影响,但快速浏览一下 libstdc++ 和 libc++(GCC 和 Clang)std::basic_streambuf 类表明它们不使用原子变量,因此它们可能会在以下情况下产生竞争条件用于缓冲。 (另一方面,libc++ sync_with_stdio 实际上什么都不做,所以调用它也没关系。)

    如果您想获得额外的性能而不考虑锁定,sync_with_stdio(false) 是一个好主意。但是,在这样做之后,锁定是必要的,如果锁定是分开的,还需要锁定 cerr.tie( nullptr )

    【讨论】:

    • 如果你确实想要单独的锁定,我有 created 一个库。
    【解决方案5】:

    这可能有用;)

    inline static void log(std::string const &format, ...) {
        static std::mutex locker;
    
        std::lock_guard<std::mutex>(locker);
    
        va_list list;
        va_start(list, format);
        vfprintf(stderr, format.c_str(), list);
        va_end(list);
    }
    

    【讨论】:

      【解决方案6】:

      我使用这样的东西:

      // Wrap a mutex around cerr so multiple threads don't overlap output
      // USAGE:
      //     LockedLog() << a << b << c;
      // 
      class LockedLog {
      public:
          LockedLog() { m_mutex.lock(); }
          ~LockedLog() { *m_ostr << std::endl; m_mutex.unlock(); }
      
          template <class T>
          LockedLog &operator << (const T &msg)
          {
              *m_ostr << msg;
              return *this;
          }
      
      private:
          static std::ostream *m_ostr;
          static std::mutex m_mutex;
      };
      
      std::mutex LockedLog::m_mutex;
      std::ostream* LockedLog::m_ostr = &std::cerr;
      

      【讨论】:

        猜你喜欢
        • 2010-12-01
        • 1970-01-01
        • 1970-01-01
        • 2011-04-16
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2017-04-30
        • 1970-01-01
        相关资源
        最近更新 更多