我在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正在刷新缓冲区,而另一个线程正在尝试putc换行符并且遇到溢出。
可能的解决方案(但有问题)可能是: (1)有一个独立的线程安全记录器类可以用于所有日志输出,因此我们可以在所有地方使用logger :: cout而不是使用std :: cout - 这是繁琐的,因为日志遍布整个地方。另外,为了使这个线程安全,互斥锁和解锁需要在每次尝试调用插入操作符之前和之后<<或像endl这样的操纵。这是一个性能打击。 (2)我们可以使用'\ n'来代替使用endl,这样就不会在每个新行插入时强制刷新,而是在需要时通过底层的ostream缓冲机制进行刷新。但是,这个线程安全吗?不确定。 (3)切换到C ++ 11,因为C ++ 11的std :: cout应该是线程安全的。但这不可能立即实现。
通过并发线程从endl操纵器中消除SIGSEGV的任何其他更好的替代方案或想法?
我可以在调用endl时以某种方式预测同步/互斥吗?
答案 0 :(得分:4)
这不仅仅是endl,整个输出流是共享的。它必须是那样的。这是一个共同的资源。图书馆不知道你想要的序列化。您必须在代码中添加它。
关于如果不序列化输出会发生什么。即使您以某种方式设法避免运行时错误,不同的输出也可以彼此混淆。因此,您必须确定程序中输出的原子单位,并将它们序列化。
答案 1 :(得分:3)
如果您正在使用C ++ 11,则可以从更多内容访问公共对象 必须保护一个线程。有一个例外,如果 没有任何访问改变对象,并且有一个特殊的 标准iostream对象的异常(但不是 一般),但即便如此,标准明确表示 个别字符可能是交错的,所以这个例外 真的不给你买任何东西;它会阻止核心转储, 但是不会阻止输出被胡言乱语,所以你需要 甚至那时某种同步。
在前C ++ 11中,每个实现都有自己的规则;有些甚至
在所有流中使每个<<
原子。但是给出了类似的东西:
std::cout << a << b;
,没有人保证不会发生另一个线程的输出
在a
的输出和b
的输出之间,所以这真的
没有给你买任何东西。
结果是您确实需要某种线程安全记录器
类。通常,此类记录器类将收集数据
一个实例本地“收集器”。这可能是std::string
或std::vector<char>
,嵌入自定义streambuf
,
知道日志记录,在时间戳上插入时间戳
前面等等,非常重要的是,确保完整的日志
记录在记录的末尾以原子方式输出。我通常
通过使用某种转发记录器类来管理它
被实例化为每个日志记录的临时值,并通知
底层streambuf(每个线程一个)每次都是
建造和破坏,所以streambuf可以照顾
其余的部分。如果你不需要时间戳等等,你
通过实现streambuf可以做到这一点有点简单
除了显式调用之外,从不输出到最终目的地
到flush
。 (这确实需要客户端的一些纪律
确保在适当的时刻调用flush
。
临时包装解决方案具有处理的优点
这或多或少是自动的。)
最后,除了小小的丢弃程序,你永远不应该
输出到std::cout
。你要么输出到某种记录器
对象(或从这样的对象获得的流),或者输出
到作为函数参数传递的std::ostream&
。
设置输出和实际输出是两个独立的
关注,通常会在不同地方处理
该程序。执行输出的代码只处理一个
std::stream
,它是从其他地方收到的。
如果您正在处理大量现有代码,那就是
没有考虑这个原则而写的:你
总是有可能修改输出的streambuf
std::cout
。这不能解决交错问题,
但它可以做成线程安全,否则至少你
不会崩溃。
答案 2 :(得分:0)
我从未详细考虑过您的问题,所以这只是对我如何解决问题的快速猜测,但它可能存在重大缺陷。
基本上,我会围绕保护流操作符的流编写一个包装类,并为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.
}
};
请注意,这会导致输出的主要开销,具体取决于您的情况,您可能希望在每个线程中写入单独的流并稍后将它们组合起来。