线程安全的cout技术.我错过了什么吗?

Chr*_*ris 9 c++ multithreading iostream cout thread-safety

我正在为游戏项目使用一些多线程代码,并且厌倦了使用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);
Run Code Online (Sandbox Code Playgroud)

执行:

#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;
}
Run Code Online (Sandbox Code Playgroud)

用法:

cout<<OUTHACK<<val1<<val2<<val3....<<endl;
Run Code Online (Sandbox Code Playgroud)

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

我已经将一些时间/线程ID调试信息添加到输出中以进行验证.到目前为止,我的测试显示此方法有效.我有几个线程用多个参数敲击cout,一切都以正确的顺序出现.

根据我在研究这个问题时所阅读的内容,cout中缺乏线程安全性似乎是人们在进入线程编程时遇到的一个非常常见的问题.我想弄清楚的是,如果我使用的技术是一个简单的问题解决方案,或者我认为我很聪明但缺少一些重要的东西.

根据我的经验,用于描述编程的聪明一词只是延迟疼痛的代码词.我在这里做些什么,或者只是在圈子里追逐糟糕的黑客?

谢谢!

R. *_*des 21

什么不是线程安全不是cout本身.它按顺序调用两个函数调用.std::cout << a << b大致相当于operator<<(std::cout, a)跟随呼叫operator<<(std::cout, b).按顺序调用两个函数并不能保证它们将以原子方式执行.

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

你可以有operator<<你的OutputStreamHack回报率按值,在析构函数解锁的对象.由于临时数据存活到每个完整表达式的结尾,代码将保持锁定"直到分号".但是,由于副本均可受累,这可能是有问题的没有转移构造函数(或C++ 03的自定义拷贝构造函数,相似auto_ptr喘气).

另一种选择是使用现有的线程安全性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;
Run Code Online (Sandbox Code Playgroud)

  • 啊,我看到了我思路上的差距.我的假设是cout行为是递归的而不是迭代的,并且它正在处理在处理OUTHACK时合并到stdout的本地流.你的技术看起来就像我所缺少的!万分感谢. (3认同)