如何使 C++ endl 操纵器线程安全?
Posted
技术标签:
【中文标题】如何使 C++ endl 操纵器线程安全?【英文标题】:How to make C++ endl manipulator thread safe? 【发布时间】:2014-01-07 18:03:27 【问题描述】:我有一个 C++ 多线程程序。我在尝试通过多个线程和程序崩溃在日志中打印某些内容时遇到的问题。具体问题是我有 cout
ff308edc _IO_do_write (ff341f28, ff341f6f, 2, ff341f6f, fc532a00, ff141f74) + dc
ff3094d8 _IO_file_overflow (ff341f28, a, ff000000, 1c00, 0, fffc00) + 2a8
ff3101fc overflow__7filebufi (ff341f28, a, 0, 1ffee, 7f2082, ff1b4f18) + 8
ff314010 overflow__8stdiobufi (a, a, ff314000, 4, fc532a00, fbdfbd51) + 10
ff306dd4 __overflow (ff341f28, a, 4, ff1b5434, ff1b5784, 82c8c) + 20
ff30fdd0 _IO_putc (a, ff341f28, 7d5be4, ff314048, ff1b5784, 82c8c) + 34
ff313088 endl__FR7ostream (7d5be0, 20, fbdfbd4e, 1, 0, 76f) + c
ff32a3f8 __ls__7ostreamPFR7ostream_R7ostream (7d5be0, 3bfb74, 3bf800, 385cd8, 76f, 0) + 4
在另一个线程上,我有:
--- called from signal handler with signal 11 (SIGSEGV) ---
ff312f20 flush__7ostream (7d5be0, a, 4, ff1b5434, ff1b5784, 82c8c) + 10
ff312f58 flush__FR7ostream (7d5be0, ff341f28, 7d5be4, ff314048, ff1b5784, 82c8c) + 4
ff313090 endl__FR7ostream (7d5be0, 20, fbffbd4e, 1, 0, 232a) + 14
std::cout 被缓冲,std::endl 强制刷新输出流。因此,似乎在一个线程上 endl 正在刷新缓冲区,而另一个线程正在尝试放置换行符并遇到溢出。
可能的解决方案(但有问题)可能是: (1) 有一个可用于所有日志输出的独立线程安全记录器类,因此我们可以在所有地方使用 logger::cout 代替 std::cout ——这很乏味,因为日志记录分散在各处。此外,为了使这个线程安全,互斥锁和解锁需要在每次尝试调用插入运算符
还有其他更好的选择或想法来摆脱并发线程从 endl 操纵器引起的 SIGSEGV?
我可以在调用 endl 时以某种方式预先进行同步/互斥吗?
【问题讨论】:
问题是关于cout
还是 endl
?还是一般的流?
std::cout 是所有线程共享的资源。因此,您应该使用锁或互斥锁小心地保护它。
您可以实现一个并发消息队列,线程将消息排入队列。请参阅有关该主题的Herb Sutter presentation(我为博客完成了实现它的练习,它似乎在 gcc 4.7 和 4.8 以及最新版本的 clang 上运行良好。)
endl,更准确地说。我猜 std::cout 应该是线程安全的。为每个日志输出使用互斥锁是乏味的,而且还会影响性能。
@DebasishJana 我闻到过早的优化。如果使用互斥锁太多,请考虑根本不记录,因为格式化用于记录的字符串可能比使用互斥锁要昂贵得多(它的额外好处是您的代码实际上可以正常工作)
【参考方案1】:
不只是 endl,整个输出流都是共享的。真的必须是这样。这是一个单一的公共资源。并且库不知道您想要的序列化。您必须在代码中添加它。
这就是如果你不序列化输出会发生什么。即使您以某种方式设法避免运行时错误,不同的输出也可能相互混淆。因此,您必须确定程序中输出的原子单位,并将它们序列化。
【讨论】:
C++11 确实保证不使用标准流会导致竞争条件。有了这个保证,你就不会崩溃。 (但无论如何,字符都可以交错,所以保证不会给你带来太多好处。)【参考方案2】:如果你使用的是 C++11,任何从更多的公共对象的访问 必须保护一个以上的线程。有一个例外,如果 没有任何访问会改变对象,并且有一个特殊的 标准 iostream 对象的例外(但不是流 一般),但即便如此,标准也明确表示 个别字符可能交错,所以这个例外 真的不给你买任何东西;它将防止核心转储, 但不会阻止输出乱码,所以你需要 即使在那时也有某种同步。
在 C++11 之前的版本中,每个实现都有自己的规则;有些甚至
在所有流中使每个<<
成为原子。但考虑到类似:
std::cout << a << b;
, none 保证不会发生来自另一个线程的输出
在a
的输出和b
的输出之间,所以这真的
没给你买任何东西。
结果是您确实需要某种线程安全的记录器
班级。通常,此类记录器类将收集数据
实例本地“收集器”。这可能是std::string
或std::vector<char>
,嵌入自定义streambuf
,
它知道日志记录,在
前端等,而且非常重要的是,确保完整的日志
记录在记录的末尾以原子方式输出。我通常
通过使用某种转发记录器类来管理它,它
被实例化为每个日志记录的临时记录,并通知
每次都是底层的streambuf(每个线程一个)
构造和销毁,所以streambuf可以处理
其余的部分。如果您不需要诸如时间戳等之类的东西,您
可以通过实现一个streambuf来更简单地做到这一点
除非在显式调用中,否则永远不会输出到最终目的地
到flush
。 (这确实需要客户端的一些纪律
侧,以确保在适当的时候调用flush
。
临时包装器解决方案的优点是处理
这或多或少是自动的。)
最后,除了小型的一次性程序,你不应该
输出到std::cout
。您要么输出到某种记录器
对象(或从此类对象获得的流),或者您输出
到作为参数传递给您的函数的std::ostream&
。
设置输出和实际输出是两个独立的
问题,通常会在不同的地方处理
该程序。执行输出的代码只处理一个
std::stream
,它是从其他地方收到的。
如果您要处理大量现有代码,即
在没有考虑到这个原则的情况下编写:你
总是有可能修改的输出streambuf
std::cout
。这不会解决交错的问题,
但它可以被做成线程安全的,所以至少你
不会崩溃。
【讨论】:
【参考方案3】:我从来没有详细考虑过你的问题,所以这只是对我将如何解决你的问题的快速猜测,但它可能存在重大缺陷。
基本上,我会围绕流编写一个包装类,以保护流运算符并赋予SomeManipulator
特殊含义(如std::endl
)。
template <class T>
struct Wrapper
Wrapper( T& stream );
template <class U>
Wrapper& operator<<( const U& u )
lock if thread does not hold the lock.
forward u to stream.
Wrapper& operator<<( SomeManipulator )
pre-cond: thread holds lock. // I.e., you can't print empty lines.
forward std::endl to stream.
unlock.
;
请注意,这会带来很大的输出开销,根据您的情况,您可能希望将每个线程写入一个单独的流,然后再将它们组合起来。
【讨论】:
以上是关于如何使 C++ endl 操纵器线程安全?的主要内容,如果未能解决你的问题,请参考以下文章