【问题标题】:Thread safety of std::cout insertion operatorstd::cout 插入运算符的线程安全
【发布时间】:2022-01-05 12:49:36
【问题描述】:

我一直认为使用std::cout << something 是线程安全的。

这个小例子

#include <iostream>
#include <thread>

void f()
{
   std::cout << "Hello from f\n";
}

void g()
{
   std::cout << "Hello from g\n";
}

int main()
{
   std::thread t1(f);
   std::thread t2(g);
   t1.join();
   t2.join();
}

我的期望是两个输出的顺序是未定义的(实际上这是我在实践中观察到的),但对 operator&lt;&lt; 的调用是线程安全的。

但是,ThreadSanitizer、DRD 和 Helgrind 似乎都给出了关于访问 std::__1::ios_base::width(long) 和 std::__1::basic_ios::fill()

在编译器资源管理器上我不see any errors

在 FreeBSD 13 上,ThreadSanitizer 给了我 3 个警告,上面列出的两个加上底层 i/o 缓冲区的 malloc/memcpy。

同样在 FreeBSD 13 中,DRD 给出了 4 个错误,width()fill() 是两个线程的两倍。

最后,FreeBSD 13 Helgrind 在线程创建中给出了一个已知的与 TLS 相关的误报,fill()width() 两次。

在 Fedora 34 上

  • g++ 11.2.1 和 ThreadSanitizer 没有错误
  • DRD 抱怨 fwrite 中的 malloc/memcpy 使用 g++ 编译的 exe
  • Helgrind 还抱怨 fwrite 以及 cout 的构造,再次使用 g++ 编译的 exe
  • clang++ 12 ThreadSanitizer 抱怨fill()width()
  • 带有 clang++ 编译器 exe 的 DRD 抱怨 fill()width()fwrite 和另一个 start_thread
  • 带有 clang++ exe 的 Helgrind 抱怨一些 TLS、fill()width()fwrite

macOS XCode clang++ ThreadSanitizer 也会生成警告(将是 libc++)。

查看 libc++ 和 libstdc++ 代码,我看不到任何保护 width() 的东西。所以我不明白为什么编译器资源管理器没有投诉。

我尝试使用 TSAN_OPTIONS=print_suppressions=1 运行,但没有更多输出 (g++ Fedora ThreadSanitizer)

似乎对width()fill() 调用达成了一些共识。

更仔细地查看 libstdc++ 源代码,我发现有 (带有一些修剪和cmets):

// ostream_insert.h
// __n is the length of the string pointed to by __s
  template<typename _CharT, typename _Traits>
    basic_ostream<_CharT, _Traits>&
    __ostream_insert(basic_ostream<_CharT, _Traits>& __out,
             const _CharT* __s, streamsize __n)
{
    typedef basic_ostream<_CharT, _Traits>       __ostream_type;
    typedef typename __ostream_type::ios_base    __ios_base;

    typename __ostream_type::sentry __cerb(__out);
    if (__cerb)
    {
        __try
        {
            const streamsize __w = __out.width();
            if (__w > __n)
            {
                // snipped
                // handle padding
            }
            else
              __ostream_write(__out, __s, __n);
          // why no hazard here?
          __out.width(0);
      }

__out 是流对象,在这种情况下是全局cout。我没有看到锁或原子之类的东西。

关于 ThreadSanitizer/g++ 如何获得“干净”输出的任何建议?

这个有点神秘的评论


  template<typename _CharT, typename _Traits>
    basic_ostream<_CharT, _Traits>::sentry::
    sentry(basic_ostream<_CharT, _Traits>& __os)
    : _M_ok(false), _M_os(__os)
    {
      // XXX MT
      if (__os.tie() && __os.good())
    __os.tie()->flush();

libc++ 代码看起来很相似。在iostream

template<class _CharT, class _Traits>
basic_ostream<_CharT, _Traits>&
__put_character_sequence(basic_ostream<_CharT, _Traits>& __os,
                          const _CharT* __str, size_t __len)
{
#ifndef _LIBCPP_NO_EXCEPTIONS
    try
    {
#endif // _LIBCPP_NO_EXCEPTIONS
        typename basic_ostream<_CharT, _Traits>::sentry __s(__os);
        if (__s)
        {
            typedef ostreambuf_iterator<_CharT, _Traits> _Ip;
            if (__pad_and_output(_Ip(__os),
                                 __str,
                                 (__os.flags() & ios_base::adjustfield) == ios_base::left ?
                                     __str + __len :
                                     __str,
                                 __str + __len,
                                 __os,
                                 __os.fill()).failed())
                __os.setstate(ios_base::badbit | ios_base::failbit);

locale


template <class _CharT, class _OutputIterator>
_LIBCPP_HIDDEN
_OutputIterator
__pad_and_output(_OutputIterator __s,
                 const _CharT* __ob, const _CharT* __op, const _CharT* __oe,
                 ios_base& __iob, _CharT __fl)
{
    streamsize __sz = __oe - __ob;
    streamsize __ns = __iob.width();
    if (__ns > __sz)
        __ns -= __sz;
    else
        __ns = 0;
    for (;__ob < __op; ++__ob, ++__s)
        *__s = *__ob;
    for (; __ns; --__ns, ++__s)
        *__s = __fl;
    for (; __ob < __oe; ++__ob, ++__s)
        *__s = *__ob;
    __iob.width(0);
    return __s;
}

我再次看到没有线程保护,但这次工具检测到了危险。

这些是真正的问题吗?对于对operator&lt;&lt; 的普通调用,width 的值不会改变,并且始终为 0。

【问题讨论】:

  • 很高兴知道。 godlbolt 很适合分享,但在不知道引擎盖下到底发生了什么的情况下,它对于像这样的高度具体的情况不太有用
  • 我刚刚在 ubuntu 上检查过,没有任何 sanitizer 错误,所以也许 Godbolt 现在并没有做任何特别的事情。

标签: multithreading c++11 valgrind thread-sanitizer


【解决方案1】:

libstdc++ 不会产生错误,而 libc++ 会产生错误。

iostream.objects.overview 多线程并发访问同步 ([ios.members.static]) 标准 iostream 对象的格式化和未格式化输入 ([istream]) 和输出 ([ostream]) 函数或标准 C 流不会导致数据竞争([intro.multithread])。

所以这在我看来像是一个 libc++ 错误。

【讨论】:

  • 很难理解标准语言的确切含义。那只是它派生的 basic_ostream 或 basic_ostream 和 basic_ios 吗?
  • @PaulFloyd 该标准明确说明了每个函数或函数组是否表现为格式化/未格式化的输入/输出函数。特别是,operator&lt;&lt;(..., const char*) 是一个格式化的输出函数,指定为here。所有这些都应该是线程安全的除非您使用sync_with_stdio(false)(即使流不同步)。
  • 我还在 FreeBSD 上看到了 width 和 g++/libstdc++ 的错误。所以目前我假设在 Linux 上使用了一些我还没有找到的线程安全机制。
  • "上面列出的两个"那些是libc++符号,你确定你在运行libstdc++吗?
  • 是的,ldd std_cout_thread_g++ std_cout_thread_g++: libstdc++.so.6 => /home/paulf/tools/gcc/lib/libstdc++.so.6 (0x800400000)
【解决方案2】:

我从 Jonathan Wakely 那里得到了答案。让我觉得自己很愚蠢。

不同之处在于,在 Fedora 上,libstdc++.so 包含 iostream 类的显式实例化。 libstdc++.so 没有针对 ThreadSanitizer 进行检测,因此它无法检测到与其相关的任何危害。

【讨论】:

    猜你喜欢
    • 2017-04-30
    • 1970-01-01
    • 2020-08-24
    • 2013-01-21
    • 1970-01-01
    • 1970-01-01
    • 2011-09-30
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多