繁体   English   中英

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

[英]How to make C++ endl manipulator thread safe?

我有一个C ++的多线程程序。 尝试通过多个线程和程序崩溃在日志中打印内容时遇到的问题。 具体的问题是我有cout <<“ some log message” << 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时以某种方式阻止同步/互斥吗?

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

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

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

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

std::cout << a << b;

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

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

最后,除非是小的一次性程序,否则永远不要输出到std::cout 您可以输出到某种记录器对象(或从这样的对象获得的流),或者输出到std::ostream&作为参数传递给您的函数。 设置输出和实际输出是两个独立的问题,通常会在程序的不同位置处理。 执行输出的代码只处理从其他地方收到的std::stream

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

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

基本上,我会围绕保护流操作符的流编写一个包装类,并为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.
    }
};

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

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM