首页 文章

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

提问于
浏览
2

我在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 类,因此我们可以在所有地方使用logger :: cout而不是使用std :: cout - 这是由于伐木遍布整个地方,所以很乏味 . 另外,为了使这个线程安全,互斥锁和解锁需要在每次尝试调用插入操作符<<或像endl之类的操纵符之前和之后 . 这是一个性能打击 . (2)我们可以使用'\ n'来代替使用endl,这样就不会在每个新行插入时强制刷新,而是在需要时通过底层的ostream缓冲机制进行刷新 . 但是,这个线程安全吗?不确定 . (3)切换到C 11,因为C 11的std :: cout应该是线程安全的 . 但这不可能立即实现 .

任何其他更好的替代或想法摆脱由并发线程的endl操纵器导致的SIGSEGV?

在调用endl时,我可以以某种方式预先同步/互斥吗?

3 回答

  • 4

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

    关于如果你没有序列化输出会发生什么 . 即使您以某种方式设法避免运行时错误,不同的输出也可以相互混淆 . 因此,您必须确定程序中输出的原子单位,并对其进行序列化 .

  • 3

    如果您使用的是C 11,则必须保护对来自多个线程的公共对象的任何访问 . 如果没有任何访问改变对象,则存在异常,并且标准iostream对象存在特殊异常(但通常不是流),但即便如此,标准清楚地表明单个字符可能是交错的,因此例外真的不会给你买任何东西;它会阻止核心转储,但不会阻止输出乱码,所以即使这样你也需要某种同步 .

    在C 11之前,每个实施都有自己的规则;有些甚至在所有流中都使每个 << 原子 . 但鉴于以下情况:

    std::cout << a << b;
    

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

    结果是您确实需要某种线程安全的 Logger 类 . 通常,此类 Logger 类将在本地"collector"实例中收集数据 . 这可能是 std::stringstd::vector<char> ,嵌入到自定义 streambuf 中,它知道日志记录,在前面插入时间戳等,而且非常重要的是,确保完整的日志记录在原点结束时输出 . 记录 . 我通常通过使用某种转发 Logger 类来管理它,它被实例化为每个日志记录的临时记录,并在每次构造和销毁时通知底层的streambuf(每个线程一个),因此streambuf可以处理休息 . 如果你不需要时间戳等东西,你可以通过实现一个从不输出到最终目的地的streambuf来做到这一点,除非显式调用 flush . (这确实需要客户端的一些规则,以确保在适当的时刻调用 flush . 临时包装器解决方案具有或多或少自动处理它的优势 . )

    最后,除了小型一次性程序外,你不应该输出到 std::cout . 您可以输出到某种 Logger 对象(或从这样的对象获得的流),或者输出到 std::ostream& 作为参数传递给您的函数 . 设置输出和实际输出是两个独立的问题,通常在程序的不同位置处理 . 执行输出的代码只处理从其他地方收到的 std::stream .

    如果您正在处理大量现有代码,而这些代码是在没有考虑此原则的情况下编写的:您始终可以修改 std::cout 的输出streambuf . 这不会解决交错的问题,但是可以使其成为线程安全的,否则至少你不会崩溃 .

  • 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.
        }
    };
    

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

相关问题