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

时间:2013-12-19 08:54:59

标签: c++ multithreading

我在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时以某种方式预测同步/互斥吗?

3 个答案:

答案 0 :(得分:4)

这不仅仅是endl,整个输出流是共享的。它必须是那样的。这是一个共同的资源。图书馆不知道你想要的序列化。您必须在代码中添加它。

关于如果不序列化输出会发生什么。即使您以某种方式设法避免运行时错误,不同的输出也可以彼此混淆。因此,您必须确定程序中输出的原子单位,并将它们序列化。

答案 1 :(得分:3)

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

在前C ++ 11中,每个实现都有自己的规则;有些甚至 在所有流中使每个<<原子。但是给出了类似的东西:

std::cout << a << b;

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

结果是您确实需要某种线程安全记录器 类。通常,此类记录器类将收集数据 一个实例本地“收集器”。这可能是std::stringstd::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.
    }
};

请注意,这会导致输出的主要开销,具体取决于您的情况,您可能希望在每个线程中写入单独的流并稍后将它们组合起来。