2012-03-02 56 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