【问题标题】:Is cout synchronized/thread-safe?cout 是否同步/线程安全?
【发布时间】:2011-09-16 11:39:14
【问题描述】:

一般来说,我假设流是不同步的,由用户来做适当的锁定。但是,像cout 这样的东西在标准库中会得到特殊处理吗?

也就是说,如果多个线程正在写入cout,它们会破坏cout 对象吗?我知道即使同步,您仍然会得到随机交错的输出,但是交错是有保证的。也就是说,在多个线程中使用cout 是否安全?

该供应商是否依赖于该供应商? gcc 是做什么的?


重要提示:如果您说“是”,请为您的回答提供某种参考,因为我需要某种证明。

我关心的也不在于底层系统调用,这些都很好,但流在顶部添加了一层缓冲。

【问题讨论】:

  • 这取决于供应商。 C++(在 C++0x 之前)没有多线程的概念。
  • c++0x 呢?它定义了一个内存模型和一个线程是什么,所以这些东西可能会在输出中漏掉?
  • 是否有供应商使其成为线程安全的?
  • 有人有最新的 C++2011 提议标准的链接吗?
  • 在某种意义上,这是printf shines作为完整输出一次性写入stdout的地方;当使用std::cout 时,表达式链的每个链接将分别输出到stdout;在它们之间可能有一些其他线程写入stdout,因此最终输出的顺序被弄乱了。

标签: c++ gcc c++11


【解决方案1】:

C++03 标准对此没有任何说明。当你无法保证某个东西的线程安全时,你应该把它当作不是线程安全的。

这里特别有趣的是cout 被缓冲了。即使保证对write(或在该特定实现中实现该效果的任何东西)的调用是互斥的,缓冲区也可能由不同的线程共享。这将很快导致流的内部状态损坏。

即使保证对缓冲区的访问是线程安全的,你认为这段代码会发生什么?

// in one thread
cout << "The operation took " << result << " seconds.";

// in another thread
cout << "Hello world! Hello " << name << "!";

您可能希望这里的每一行都相互排斥。但是实现如何保证这一点?

在 C++11 中,我们确实有一些保证。 FDIS 在 §27.4.1 [iostream.objects.overview] 中说了以下内容:

不应导致多线程同时访问同步 (§27.5.3.4) 标准 iostream 对象的格式化和未格式化输入 (§27.7.2.1) 和输出 (§27.7.3.1) 函数或标准 C 流 在数据竞赛中(§1.10)。 [注意:用户仍然必须通过以下方式同步这些对象和流的并发使用 如果他们希望避免交错字符,则可以使用多个线程。 ——尾注]

因此,您不会得到损坏的流,但如果您不希望输出是垃圾,您仍然需要手动同步它们。

【讨论】:

  • 技术上适用于 C++98/C++03,但我想每个人都知道这一点。但这并没有回答两个有趣的问题:C++0x 呢?典型的实现实际上做什么
  • @edA-qa mort-ora-y :不,你错了。 C++11 明确定义了标准流对象可以被同步并保持良好定义的行为,而不是默认情况下。
  • @ildjarn - 不,@edA-qa mort-ora-y 是正确的。只要cout.sync_with_stdio() 为真,使用cout 从多个线程输出字符而无需额外同步是明确定义的,但仅限于单个字节级别。因此,在不同线程中执行的cout &lt;&lt; "ab";cout &lt;&lt; "cd" 可能会输出例如acdb,但可能不会导致Undefined Behaviour。
  • @JohannesD :我们同意——它与底层 C API 同步。我的观点是它没有以一种有用的方式“同步”,即如果他们不想要垃圾数据,仍然需要手动同步。
  • @ildjarn,我对垃圾数据很好,我理解这一点。我只是对数据竞争条件感兴趣,现在似乎很清楚了。
【解决方案2】:

这是一个很好的问题。

首先,C++98/C++03 没有“线程”的概念。所以在那个世界里,这个问题毫无意义。

C++0x 呢?见Martinho's answer(我承认这让我很吃惊)。

C++0x 之前的具体实现如何?好吧,例如,这里是来自 GCC 4.5.2 的basic_streambuf&lt;...&gt;:sputc 的源代码(“streambuf”标头):

 int_type
 sputc(char_type __c)
 {
   int_type __ret;
   if (__builtin_expect(this->pptr() < this->epptr(), true)) {
       *this->pptr() = __c;
        this->pbump(1);
        __ret = traits_type::to_int_type(__c);
      }
    else
        __ret = this->overflow(traits_type::to_int_type(__c));
    return __ret;
 }

显然,这不会执行锁定。 xsputn 也没有。这绝对是 cout 使用的 streambuf 类型。

据我所知,libstdc++ 不对任何流操作执行锁定。而且我不会期望任何,因为那会很慢。

因此,有了这个实现,显然两个线程的输出可能会相互破坏(不是只是交错)。

这段代码会破坏数据结构本身吗?答案取决于这些功能可能的相互作用;例如,如果一个线程尝试刷新缓冲区,而另一个线程尝试调用xsputn 或其他什么,会发生什么情况。这可能取决于您的编译器和 CPU 如何决定重新排序内存加载和存储;需要仔细分析才能确定。如果两个线程同时尝试修改同一个位置,这还取决于您的 CPU 会做什么。

换句话说,即使它碰巧在您当前的环境中运行良好,当您更新任何运行时、编译器或 CPU 时,它也可能会中断。

执行摘要:“我不会”。构建一个进行适当锁定的日志记录类,或迁移到 C++0x。

作为一个较弱的替代方案,您可以将 cout 设置为无缓冲。很可能(尽管不能保证)会跳过与缓冲区相关的所有逻辑并直接调用write。虽然这可能非常慢。

【讨论】:

  • 不错的答案,但请看 Martinho 的回复,这表明 C++11 确实为 cout 定义了同步。
【解决方案3】:

C++ 标准没有指定写入流是否是线程安全的,但通常不是。

www.techrepublic.com/article/use-stl-streams-for-easy-c-plus-plus-thread-safe-logging

还有:Are standard output streams in C++ thread-safe (cout, cerr, clog)?

更新

请查看@Martinho Fernandes 的回答,了解新标准 C++11 对此有何说明。

【讨论】:

  • 我想既然 C++11 现在是标准,这个答案现在实际上是错误的。
【解决方案4】:

正如其他答案所提到的,这绝对是特定于供应商的,因为 C++ 标准没有提到线程(这在 C++0x 中发生了变化)。

GCC 并没有对线程安全和 I/O 做出很多承诺。但它所承诺的文档在这里:

关键的东西可能是:

__basic_file 类型只是一个 收集周围的小包装纸 C stdio 层(再次,请参阅链接 在结构下)。我们不锁定 我们自己,但只是通过 调用 fopen、fwrite 等。

所以,对于 3.0,“是 I/O 的多线程安全”必须是 回答:“是您平台的 C 用于 I/O 的库线程安全?”有些是 默认情况下,有些不是;许多提供 C的多种实现 库具有不同的权衡 线程安全和效率。你这个 程序员,总是需要 注意多线程。

(例如,POSIX 标准 要求 C stdio FILE* 操作 是原子的。符合 POSIX 的 C 库(例如,在 Solaris 和 GNU/Linux) 有一个内部互斥体 对 FILE*s 进行序列化操作。 但是,您仍然不需要这样做 愚蠢的事情,比如调用 fclose(fs) 在一个线程中,然后访问 fs 在另一个。)

所以,如果您平台的 C 库是 线程安全,然后是你的 fstream I/O 操作将是线程安全的 最低水平。对于更高级别 操作,例如操纵 流中包含的数据 格式化类(例如,设置 std::ofstream 内的回调), 您需要保护此类访问,例如 任何其他关键共享资源。

我不知道在提到的 3.0 时间范围内是否有任何变化。

MSVC 的iostreams 线程安全文档可以在这里找到:http://msdn.microsoft.com/en-us/library/c9ceah3b.aspx

单个对象是线程安全的 从多个线程读取。为了 例如,给定一个对象 A,它是安全的 从线程 1 和从 同时线程 2。

如果正在写入单个对象 通过一个线程,然后所有读取和 在同一或上写入该对象 其他线程必须受到保护。为了 例如,给定一个对象 A,如果线程 1 正在写入 A,那么线程 2 必须 被阻止阅读或 写给 A。

读写一个是安全的 一个类型的实例,即使另一个 线程正在读取或写入 同一类型的不同实例。 例如,给定对象 A 和 B 相同的类型,如果 A 是安全的 被写在线程 1 和 B 中 正在线程 2 中读取。

...

iostream 类

iostream 类遵循相同的 规则与其他类一样,有一个 例外。写信是安全的 来自多个线程的对象。为了 例如,线程 1 可以在以下位置写入 cout 与线程 2 相同的时间。但是, 这可能会导致从 两个线程混合在一起。

注意:从流缓冲区中读取是 不认为是读操作。 它应该被视为写 操作,因为这改变了 类的状态。

请注意,该信息适用于最新版本的 MSVC(当前适用于 VS 2010/MSVC 10/cl.exe16.x)。您可以使用页面上的下拉控件选择旧版本 MSVC 的信息(旧版本的信息不同)。

【讨论】:

  • “我不知道在提到的 3.0 时间范围内是否有任何变化。”它确实做到了。在过去的几年里,g++ 流实现已经执行了自己的缓冲。
猜你喜欢
  • 1970-01-01
  • 2013-02-15
  • 1970-01-01
  • 2017-04-30
  • 1970-01-01
  • 1970-01-01
  • 2012-11-12
  • 2019-12-13
相关资源
最近更新 更多