std::cout 插入运算符的线程安全
Thread safety of std::cout insertion operator
我一直认为使用 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<<
的调用是线程安全的。
然而,ThreadSanitizer、DRD 和 Helgrind 似乎都给出了关于访问 std::__1::ios_base::width(long) 和 std::__1::basic_ios::fill()[ 的各种错误=42=]
我没有在编译器资源管理器上 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 在使用 g++ 编译的 exe 的 fwrite 中抱怨 malloc/memcpy
- 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++ 源代码,我发现有
(有一些修剪和评论):
// 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<<
的普通调用,width
的值不会改变,并且始终为 0。
libstdc++
不会产生错误,而 libc++
会产生错误。
iostream.objects.overview Concurrent access to a synchronized ([ios.members.static]) standard iostream object's formatted and unformatted input ([istream]) and output ([ostream]) functions or a standard C stream by multiple threads does not result in a data race ([intro.multithread]).
所以这对我来说看起来像是一个 libc++ 错误。
我从 Jonathan Wakely 那里得到了答案。让我觉得自己很蠢。
不同之处在于,在 Fedora 上,libstdc++.so 包含 iostream 类 的显式实例化。 libstdc++.so 未针对 ThreadSanitizer 进行检测,因此它无法检测到任何与之相关的危险。
我一直认为使用 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<<
的调用是线程安全的。
然而,ThreadSanitizer、DRD 和 Helgrind 似乎都给出了关于访问 std::__1::ios_base::width(long) 和 std::__1::basic_ios 我没有在编译器资源管理器上 see any errors。 在 FreeBSD 13 上,ThreadSanitizer 给了我 3 个警告,上面列出的两个加上底层 i/o 缓冲区的 malloc/memcpy。 同样在 FreeBSD 13 中,DRD 给出了 4 个错误, 最后,FreeBSD 13 Helgrind 在线程创建中给出了一个与 TLS 相关的已知误报, 在 Fedora 34 上 macOS XCode clang++ ThreadSanitizer 也会生成警告(将是 libc++)。 查看 libc++ 和 libstdc++ 代码,我没有看到任何保护 我尝试了 运行 TSAN_OPTIONS=print_suppressions=1 并且没有更多的输出 (g++ Fedora ThreadSanitizer) 似乎对 仔细查看 libstdc++ 源代码,我发现有
(有一些修剪和评论): 关于 ThreadSanitizer/g++ 如何获得“干净”输出的任何建议? 有这个有点神秘的评论 libc++ 代码看起来很相似。在 并在 我再次看到没有线程保护,但这次工具也检测到了危险。 这些是真正的问题吗?对于 width()
和 fill()
是两个线程的两倍。fill()
和 width()
两次。
cout
的构造,再次使用 g++ 编译的 exefill()
和 width()
fill()
、width()
、fwrite
和 start_thread
fill()
,width()
,fwrite
width()
的东西。所以我不明白为什么编译器资源管理器上没有投诉。width()
和 fill()
呼吁达成了一些共识。// 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
。我没有看到任何类似锁或原子的东西。
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();
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<<
的普通调用,width
的值不会改变,并且始终为 0。
libstdc++
不会产生错误,而 libc++
会产生错误。
iostream.objects.overview Concurrent access to a synchronized ([ios.members.static]) standard iostream object's formatted and unformatted input ([istream]) and output ([ostream]) functions or a standard C stream by multiple threads does not result in a data race ([intro.multithread]).
所以这对我来说看起来像是一个 libc++ 错误。
我从 Jonathan Wakely 那里得到了答案。让我觉得自己很蠢。
不同之处在于,在 Fedora 上,libstdc++.so 包含 iostream 类 的显式实例化。 libstdc++.so 未针对 ThreadSanitizer 进行检测,因此它无法检测到任何与之相关的危险。