如何使C++endl操作器线程安全

How to make C++ endl manipulator thread safe?

本文关键字:线程 安全 操作器 C++endl 何使      更新时间:2023-10-16

我在C++中有一个多线程程序。我在尝试通过多个线程和程序崩溃在日志中打印内容时遇到的问题。具体的问题是我有cout<lt;"一些日志消息"<lt;endl;当我看到转储内核的pstack时,它表明endl导致了争用问题。在一个线程上,我有:

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——这很乏味,因为日志记录到处都是。此外,为了使该线程安全,互斥锁和解锁需要在每次尝试调用插入运算符<lt;或者像endl这样的操纵器。这是一个表演上的成功。(2) 我们可以使用'\n'代替endl,这样就不会在每次插入新行时强制刷新,而是在需要时由底层的ostream缓冲机制进行刷新。但是,这根线安全吗?不确定。(3) 切换到C++11,因为C++11的std::cout应该是线程安全的。但这不可能马上实现。

有其他更好的替代方案或想法可以通过并发线程消除由endl操纵器引起的SIGSEGV吗?

在调用endl时,我能以某种方式提前同步/互斥吗?

这不仅仅是endl,整个输出流都是共享的。真的必须是这样。这是一个单一的公共资源。而且库不知道您想要的序列化。您必须将其添加到代码中。

这就是如果不序列化输出会发生什么。不同的输出片段可能会相互混淆,即使您设法避免了运行时错误。因此,您必须决定程序中输出的原子单位,并对它们进行序列化。

如果您使用的是C++11,则从必须保护多个线程。如果所有的访问都不会改变对象,并且有一个特殊的标准iostream对象的异常(但不是中的流一般),但即使在那时,标准也明确表示单个字符可能是交错的,所以这个例外真的不会给你买任何东西;它将防止核心转储,但不能防止输出胡言乱语,因此您需要即使在那时也有某种同步。

在C++11之前,每个实现都有自己的规则;有些甚至使每个<<原子化。但考虑到以下情况:

std::cout << a << b;

,none保证不会发生来自另一个线程的输出在a的输出和b的输出之间,所以这真的没有给你买任何东西。

结果是,您确实需要某种线程安全记录器班通常,此类记录器类将在实例本地"收集器"。这可能是std::string或者嵌入到定制CCD_ 6中的CCD_,它知道日志记录,在front等,非常重要的是,确保完整的日志记录在记录的末尾以原子方式输出。我通常通过使用某种转发记录器类来管理它被实例化为每个日志记录的临时,并通知底层streambuf(每个线程一个)建造和销毁,所以streambuf可以处理如果你不需要时间戳之类的东西,你可以通过实现streambuf来实现这一点,该streambuf除非进行显式调用,否则从不输出到最终目的地至CCD_ 7。(这确实需要客户遵守一些纪律以确保flush在适当时刻被调用。临时包装器解决方案具有处理的优点这或多或少是自动的。)

最后,除了在小的一次性程序中,你永远不应该输出到CCD_ 9。你要么输出到某种记录器对象(或从该对象获得的流),或者您输出传递给作为函数参数的std::ostream&。设置输出和实际输出是两个独立的关注,通常将在程序。执行输出的代码只处理std::stream,它已从其他地方收到。

如果您正在处理大量现有代码写的时候没有考虑到这个原则:你始终可以修改的输出流bufstd::cout。这将不能解决交织的问题,但在其他情况下,它可以是线程安全的,所以至少你不会崩溃。

我从来没有详细考虑过你的问题,所以这只是我如何解决你的问题的一个快速猜测,但它可能有重大缺陷。

基本上,我会围绕流编写一个包装类,保护流运算符,并赋予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.
}
};

请注意,这会给输出带来很大的开销,根据您的情况,您可能更喜欢用每个线程编写单独的流,然后再将它们组合起来。