【问题标题】:Thread safe cout technique. Am I missing something?线程安全 cout 技术。我错过了什么吗?
【发布时间】:2012-03-20 14:12:36
【问题描述】:

我正在为一个游戏项目处理一些多线程代码,并且有点厌倦了对两个线程使用 cout 同时调试消息创建的 stdout 呕吐物进行排序。在想出“某事”之前,我做了一些研究,盯着墙壁看了一两个小时。以下代码使用 SFML 进行时间保持和线程化。 SFML 互斥体只是包装在窗口中的关键部分。

标题:

#include <SFML\System.hpp>
#include <iostream>

class OutputStreamHack
{
    public:
    OutputStreamHack();
    ~OutputStreamHack();

    ostream& outputHijack(ostream &os);

    private:
    sf::Clock myRunTime;
    sf::Mutex myMutex;
};

static OutputStream OUTHACK;

ostream& operator<<(ostream& os, const OutputStreamHack& inputValue);

实施:

#include <SFML\System.hpp>
#include <iostream>

#include "OutputStreamHack.h"

using namespace std;

OutputStreamHack::OutputStreamHack()
{
    myMutex.Unlock();
    myRunTime.Reset();
}

OutputStreamHack::~OutputStreamHack()
{
    myMutex.Unlock();
    myRunTime.Reset();
}

ostream& OutputStreamHack::outputHijack(ostream &os)
{

    sf::Lock lock(myMutex);
    os<<"<"<<myRunTime.GetElapsedTime()<<","<<GetCurrentThreadId()<<"> "<<flush;
    return os;
}

ostream& operator<<(ostream& os, const OutputStreamHack& inputValue)
{
    OUTHACK.outputHijack(os);
    return os;
}

用法:

cout<<OUTHACK<<val1<<val2<<val3....<<endl;

好的,所以它的工作方式是通过一个重载的插入运算符,它通过将迭代器锁定在静态对象中,然后刷新缓冲区来强制线程安全。如果我正确理解了这个过程(我主要是一个自学成才的程序员),cout 会从头到尾处理其插入链的元素,将 ostream 变量沿链向下传递,以便将每个元素添加到流中。一旦到达 OUTHACK 元素,就会调用重载的运算符,锁定互斥体,并刷新流。

出于验证目的,我在输出中添加了一些时间/线程 ID 调试信息。到目前为止,我的测试表明这种方法有效。我有几个线程用多个参数敲击 cout,一切都以正确的顺序出现。

根据我在研究这个问题时所读到的内容,cout 中缺乏线程安全似乎是人们在尝试线程编程时遇到的一个相当普遍的问题。我想弄清楚的是我使用的技术是解决问题的简单方法,还是我认为我很聪明但错过了一些重要的东西。

根据我的经验,用于描述编程的聪明这个词只是延迟痛苦的代名词。我是在这里做某事,还是只是在兜圈子追逐糟糕的黑客行为?

谢谢!

【问题讨论】:

  • 它的工作原理纯属运气。只有时间和线程 ID 的输出受互斥锁保护。完全有可能在OUTHACK 和val1 之间潜入另一个线程。
  • 考虑先写入ostringstream,然后在一次操作中将内容转储到cout。 cout 通常是线程安全的,只是锁定显然是每次调用,并且每个 &lt;&lt; 操作都是一个不同的调用......这样,您的应用程序代码中就没有额外的锁定 - 这只会降低并行性.

标签: c++ multithreading thread-safety iostream cout


【解决方案1】:

这里不是线程安全的不是cout 本身。它依次调用两个函数调用。 std::cout &lt;&lt; a &lt;&lt; b 大致相当于调用operator&lt;&lt;(std::cout, a) 后跟operator&lt;&lt;(std::cout, b)。依次调用两个函数并不能保证它们将以原子方式执行。

按原样,只有时间和线程 ID 的输出受互斥锁保护。在插入OUTHACK 和val1 之间完全有可能让另一个线程潜入,因为在插入OUTHACK 之后不再持有锁。

您可以让operator&lt;&lt; 为您的OutputStreamHack 返回按值在析构函数中解锁的对象。由于临时对象一直存在到每个完整表达式的末尾,因此代码将保持锁定“直到分号”。但是,由于可能涉及副本,如果没有移动构造函数(或 C++03 中的自定义复制构造函数,类似于 auto_ptr 的 gasp),这可能会出现问题。

另一个选择是使用cout 的现有线程安全性(由 C++11 中的语言保证,但之前许多实现都是线程安全的)。创建一个对象,将所有内容流式传输到 std::stringstream 成员中,然后在销毁时立即将其全部写入。

class FullExpressionAccumulator {
public:
    explicit FullExpressionAccumulator(std::ostream& os) : os(os) {}
    ~FullExpressionAccumulator() {
        os << ss.rdbuf() << std::flush; // write the whole shebang in one go
    }

    template <typename T>
    FullExpressionAccumulator& operator<<(T const& t) {
        ss << t; // accumulate into a non-shared stringstream, no threading issues
        return *this;
    }

private:
    std::ostream& os;
    std::stringstream ss;

    // stringstream is not copyable, so copies are already forbidden
};

// using a temporary instead of returning one from a function avoids any issues with copies
FullExpressionAccumulator(std::cout) << val1 << val2 << val3;

【讨论】:

  • 啊,我看到了我的想法的差距。我的操作是假设 cout 行为是递归的而不是迭代的,并且它正在处理一个本地流,该流在处理 OUTHACK 时合并到标准输出中。你的技术看起来正是我所缺少的!非常感谢。
  • 此解决方案可用于模仿std::osyncstream(包含在 C++20 中但尚不可用),但是,它仍然需要互斥体来防止交错/乱码输出,并且至少还有 1 个 operator&lt;&lt; 过载,以说明 std::endl。
猜你喜欢
  • 1970-01-01
  • 2014-10-31
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2018-05-09
  • 2017-04-30
  • 1970-01-01
相关资源
最近更新 更多