如何使C ++ endl操作器线程安全?
我在C ++中有一个多线程程序。 我在尝试通过多线程和程序崩溃在日志中打印某些内容时遇到的问题。 具体问题是我有cout <<“某些日志消息”<< 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)有一个可以用于所有日志输出的独立线程安全记录器类,所以不是使用std :: cout,而是可以在所有地方使用logger :: cout - 这就是由于日志遍布各处,因此繁琐。 此外,为了使线程安全,互斥锁需要在每次尝试调用插入运算符<<或像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;
,但不能保证从a
的输出和b
的输出之间不会出现另一个线程的输出,所以这真的不会给你买任何东西。
结果是你确实需要某种线程安全的记录器类。 通常,这样的记录器类将收集实例本地“收集器”中的数据。 这可能是一个std::string
或一个std::vector<char>
,嵌入到一个自定义的流streambuf
,它知道日志记录,在前面插入时间戳等,非常重要的是,确保完整的日志记录在记录结束时以原子方式输出。 我通常通过使用某种类型的转发记录器类来进行管理,该记录器类作为每个日志记录的临时实例进行实例化,并在每次构建和销毁时通知基础streambuf(每个线程一个),因此streambuf可以处理休息。 如果你不需要的东西像时间戳等,你可以做到这一点通过实现流缓冲从未输出到最终目的地,除了显式调用有些简单flush
。 (这确实需要客户端的一些训练,以确保在适当的时候调用flush
。临时包装解决方案有或多或少自动处理这个优点。)
最后,除了小丢弃程序,你不应该输出到std::cout
。 你输出到某种记录器对象(或从这样的对象中获得的流),或者输出到std::ostream&
作为参数传递给你的函数。 设置输出和实际输出是两个独立的问题,通常会在程序的不同位置处理。 执行输出的代码只是处理从其他地方接收到的std::stream
。
如果你正在处理一大堆现有的代码,而这些代码是不考虑这个原则编写的:你总是可以修改std::cout
的输出流std::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.
}
};
请注意,这会导致输出的主要开销,这取决于您的情况,您可能希望更喜欢在每个线程中写入一个单独的流并稍后将其合并。
链接地址: http://www.djcxy.com/p/61305.html