2012-03-02 91 views
8

我正在使用一些游戏项目的多线程代码,并且厌倦了通过两个线程使用cout创建的stdout呕吐物来排序,以便在同一时间调试消息。我做了一些研究,并在提出“某些事情”之前盯着墙壁一两个小时。以下代码使用SFML进行计时和线程处理。 SFML互斥锁只是将窗口中的关键部分封装起来。线程安全cout技术。我错过了什么吗?

页眉:

#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元素,就会调用重载的操作符,互斥锁被锁定,流被刷新。

为了验证目的,我在输出中添加了一些time/thread id调试信息。到目前为止,我的测试表明这种方法是有效的。我有几个线程用多个参数敲击cout,并且所有内容都以正确的顺序出现。

从我在研究这个问题时读到的内容来看,在cout中缺少线程安全似乎是人们在冒险进入线程化编程时遇到的一个相当常见的问题。我试图弄清楚的是,如果我使用的技术是解决这个问题的简单方法,或者我认为我很聪明,但错过了一些重要的东西。

根据我的经验,当用来描述编程的时候,聪明这个词只是延迟疼痛的代码字。我在这里做些什么,或者只是在周围追逐糟糕的黑客?

谢谢!

+1

它运作的事实是纯粹的运气。只有时间和线程ID的输出受互斥量保护。在'OUTHACK'和'val1'之间潜入另一个线程是完全可能的。 – 2012-03-02 02:25:11

+1

首先考虑写入'ostringstream',然后将内容在一个操作中转储到'cout'。 ''cout'通常是线程安全的,只是每次调用的锁定都很明显,每个'<<'操作都是一个独特的调用....这样,应用程序代码中就不会再有额外的锁定 - 这只能减少并行性。 – 2012-03-02 02:28:01

+0

相关:[是cout同步/线程安全吗?](http://stackoverflow.com/questions/6374264/is-cout-synchronized-thread-safe/6374525#6374525) – legends2k 2014-06-06 11:49:10

回答

19

这里没有什么线程安全不是cout本身。它依次调用两个函数调用。 std::cout << a << b大致相当于拨打operator<<(std::cout, a)后跟operator<<(std::cout, b)。按顺序调用两个函数并不能保证它们将以原子方式执行。

因为只有时间和线程ID的输出被互斥锁保护。在插入OUTHACKval1之间插入另一个线程是完全可能的,因为在插入OUTHACK之后锁不再被锁定。

您可以有operator<<为您的OutputStreamHack返回价值解析器中解锁的对象。由于临时对象直到每个完整表达式的末尾都存在,所以代码将锁定为“直到分号”。但是,因为可能涉及副本,所以如果没有移动构造函数(或C++ 03中的自定义副本构造函数,类似于auto_ptrgasp),则可能会出现问题。

另一种选择是使用现有的线程安全性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; 
+2

啊,我看到我的思想差距。我是在假设cout行为是递归的而不是迭代的假设下运行的,并且它正在处理本地流,该本地流在处理OUTHACK的位置合并到stdout中。 你的技术看起来就像我错过了!万分感谢。 – Chris 2012-03-02 02:47:00