2013-10-22 41 views
3

我一直在四處尋找各種方法來解決線程安全日誌問題,但我還沒有看到任何相當類似的東西,所以我不知道是否因爲作爲C++的一個完整的新手而沒有注意到它是某種可怕的東西,線程和iostreams。它似乎在我已經完成的基本測試中起作用。我的線程安全日誌類的方法很糟糕嗎?

基本上我有一個Log類(創意,我知道...),它具有爲標準操縱器設置的操作符< <,所以我可以快活地傳入任何我想要的東西。

但是,我知道是這樣的:

std::cout << "Threads" << " will" << " mess" << " with" << "this." << std::endl; 

將有可能得到交錯當多個線程編寫來清點(或其他地方登錄的ostream點)。所以,我創建了一些特定於日誌類操縱,讓我做到這一點:

Log::log << lock << "Write" << " what" << " I" << " want" << std::endl << unlock; 

我只是想知道,如果這是一個固有的可怕想法,記住,我願意接受軸承用戶的Log類需要通過'鎖定'和'解鎖'進行處理。我認爲使'std :: endl'自動解鎖,但這似乎會造成更多的麻煩......我認爲散佈的使用應該在測試中出現,但是如果任何人都可以看到一種方法來進行編譯,時間錯誤,那很好。

我也很感激任何建議,使我的代碼更清潔。

以下是該課堂的簡化版本,用於演示目的;整個事情有更多的構造函數採用類似文件名的東西,所以與問題無關。

#include <iostream> 
#include <thread> 
#include <fstream> 

class Log{ 
public: 
    //Constructors 
    Log(std::ostream & os); 
    // Destructor 
    ~Log(); 
    // Input Functions 
    Log & operator<<(const std::string & msg); 
    Log & operator<<(const int & msg); 
    Log & operator<<(std::ostream & (*man)(std::ostream &)); // Handles manipulators like endl. 
    Log & operator<<(std::ios_base & (*man)(std::ios_base &)); // Handles manipulators like hex. 
    Log & operator<<(Log & (*man)(Log &)); // Handles custom Log manipulators like lock and unlock. 
    friend Log & lock(Log & log); // Locks the Log for threadsafe output. 
    friend Log & unlock(Log & log); // Unlocks the Log once threadsafe output is complete. 
private: 
    std::fstream logFile; 
    std::ostream & logStream; 
    std::mutex guard; 
}; 

// Log class manipulators. 
Log & lock(Log & log); // Locks the Log for threadsafe output. 
Log & unlock(Log & log); // Unlocks the Log once threadsafe output is complete. 

void threadUnsafeTask(int * input, Log * log); 
void threadSafeTask(int * input, Log * log); 

int main(){ 
    int one(1), two(2); 
    Log log(std::cout); 
    std::thread first(threadUnsafeTask, &one, &log); 
    std::thread second(threadUnsafeTask, &two, &log); 
    first.join(); 
    second.join(); 
    std::thread third(threadSafeTask, &one, &log); 
    std::thread fourth(threadSafeTask, &two, &log); 
    third.join(); 
    fourth.join(); 
    return 0; 
} 

void threadUnsafeTask(int * input, Log * log){ 
    *log << "Executing" << " thread '" << *input << "', " << "expecting " << "interruptions " << "frequently." << std::endl; 
} 

void threadSafeTask(int * input, Log * log){ 
    *log << lock << "Executing" << " thread '" << *input << "', " << "not expecting " << "interruptions." << std::endl << unlock; 
} 

// Constructors (Most left out as irrelevant) 
Log::Log(std::ostream & os): logFile(), logStream(logFile), guard(){ 
    logStream.rdbuf(os.rdbuf()); 
} 

// Destructor 
Log::~Log(){ 
    logFile.close(); 
} 

// Output Operators 
Log & Log::operator<<(const std::string & msg){ 
    logStream << msg; 
    return *this; 
} 

Log & Log::operator<<(const int & msg){ 
    logStream << msg; 
    return *this; 
} 

Log & Log::operator<<(std::ostream & (*man)(std::ostream &)){ 
    logStream << man; 
    return *this; 
} 

Log & Log::operator<<(std::ios_base & (*man)(std::ios_base &)){ 
    logStream << man; 
    return *this; 
} 

Log & Log::operator<<(Log & (*man)(Log &)){ 
    man(*this); 
    return *this; 
} 

// Manipulator functions. 
Log & lock(Log & log){ 
    log.guard.lock(); 
    return log; 
} 

Log & unlock(Log & log){ 
    log.guard.unlock(); 
    return log; 
} 

它爲我在Ubuntu12.04克++編譯:

g++ LogThreadTest.cpp -o log -std=c++0x -lpthread 

相關,以使定製操縱是無恥地從here但那兒剽竊不要怪他們我的無能copypasta位。

+3

這是一個錯誤在這裏不使用RAII。 –

+0

恕我直言,一個無鎖FIFO將是一個更好的主意在這裏..以某種方式線程暫停記錄不吸引力。 – vrdhn

+2

依靠用戶鎖定和解鎖東西是不可靠的。一種選擇是讓您的日誌文件同時從用戶那裏獲取消息,並通過將它們放入單個隊列並在單獨的線程中運行它們來序列化它們。所以從多用戶的角度來看,這些呼叫是非阻塞的,但實際上沒有交織。參見[Herb Sutter的這篇演講](http://channel9.msdn.com/Shows/Going+Deep/C-and-Beyond-2012-Herb-Sutter-Concurrency-and-Parallelism)瞭解更多信息。我經歷了實現他的併發對象包裝的工作版本的練習。 – juanchopanza

回答

4

這是一個壞主意。 想象一下:

void foo() 
{ 
    throw std::exception(); 
} 

log << lock << "Write" << foo() << " I" << " want" << std::endl << unlock; 
         ^
          exception! 

這使得你的Log鎖定。這是不好的,因爲其他線程可能正在等待鎖定。 這也發生在你每次只需忘記unlock。 你應該在這裏使用RAII:

// just providing a scope 
{ 
    std::lock_guard<Log> lock(log); 
    log << "Write" << foo() << " I" << " want" << std::endl; 
} 

你需要調整你的lockunlock方法有簽名void lock()void unlock(),使它們的類Log的成員函數。


另一方面,這是相當龐大。請注意,在C++ 11中,使用std::cout是線程安全的。所以你可以很容易地做

std::stringstream stream; 
stream << "Write" << foo() << " I" << " want" << std::endl; 
std::cout << stream.str(); 

這是完全沒有額外的鎖。

+0

啊哈!是的,我錯過了缺乏安全性的例外,謝謝! – umlimo

+1

提及異常相關問題+1! – thokra

+0

關於接受哪個答案的艱難決定,但即使Useless的答案提供了一個很好的解決方案,但這個答案更直接。爲所有人歡呼。 – umlimo

3

你並不需要顯式地經過鎖定機械手,你可以使用一個哨兵(帶RAII語義,漢斯帕桑特說)

class Log{ 
public: 
    Log(std::ostream & os); 
    ~Log(); 

    class Sentry { 
     Log &log_; 
    public: 
     Sentry(Log &l) log_(l) { log_.lock(); } 
     ~Sentry() { log_.unlock(); } 

     // Input Functions just forward to log_.logStream 
     Sentry& operator<<(const std::string & msg); 
     Sentry& operator<<(const int & msg); 
     Sentry& operator<<(std::ostream & (*man)(std::ostream &)); // Handles manipulators like endl. 
     Sentry& operator<<(std::ios_base & (*man)(std::ios_base &)); // Handles manipulators like hex. 
    }; 

    template <typename T> 
    Sentry operator<<(T t) { return Sentry(*this) << t; } 
    void lock(); 
    void unlock(); 

private: 
    std::fstream logFile; 
    std::ostream & logStream; 
    std::mutex guard; 
}; 

現在,寫

Log::log << "Write" << " what" << " I" << " want" << foo() << std::endl; 

將:

  1. 創建一個臨時哨兵對象
    • 其鎖定日誌對象
  2. ...轉發每個operator<<呼叫到母體日誌實例...
  3. 然後超出範圍在表達式的結尾(或如果foo拋出)
    • 其解鎖日誌對象

Althou這是安全的,它也引發了很多爭論(互斥鎖被鎖定的時間比我通常喜歡的要長,同時格式化消息)。較低爭用的方法是根據本地存儲(本地線程或本地範圍)進行格式化,然後保持足夠長的時間以將其移入共享日誌記錄隊列。

+1

這該死的,那是優雅的...... –

+0

啊,我完全誤解了Hans Passant對RAII的提及,但是這澄清了它,這是一個美麗的解決方案,謝謝!爲什麼我找不到這個優雅的方法? :D – umlimo

+0

恐怕我不記得我第一次看到這個想法的地方。順便說一句,你可以通過給Sentry一個std :: unique_lock成員來清理它,而不是直接調用鎖定和解鎖(然後你可以免費移動ctor和dtor)。 – Useless

2

這不是一個非常好的主意,因爲有人會致命 忘記unlock在某些時候,導致所有線程 掛在下一個日誌。還有一個問題,如果你正在登錄的表達式之一會拋出一個。 (這不應該 發生,因爲你不希望在日誌 聲明實際行爲,並沒有任何行爲不應 扔東西,但你永遠不知道。)

的通常的日誌記錄解決方案是使用一個特殊的臨時對象,它在構造函數中獲取鎖,並將其釋放到析構函數中(並且也會刷新並確保有 尾隨'\n')。這可以在C++ 11, 使用移動語義非常優雅的完成(因爲你一般要在一個函數來創建臨時的 實例,但其臨時析構函數 行爲應該是函數外);在C++ 03中,您需要允許複製 ,並確保它只是釋放鎖的最終副本 。

粗略地說,你Log類看起來是這樣的:

struct LogData 
{ 
    std::unique_lock<std::mutex> myLock 
    std::ostream myStream; 

    LogData(std::unique_lock<std::mutex>&& lock, 
      std::streambuf* logStream) 
     : myLock(std::move(lock)) 
     , myStream(logStream) 
    { 
    } 

    ~LogData() 
    { 
     myStream.flush(); 
    } 
}; 

class Log 
{ 
    LogData* myDest; 
public: 
    Log(LogData* dest) 
     : myDest(dest) 
    { 
    } 
    Log(Log&& other) 
     : myDest(other.myDest) 
    { 
     other.myDest = nullptr; 
    } 
    ~Log() 
    { 
     if (myDest) { 
      delete myDest; 
     } 
    } 
    Log& operator=(Log const& other) = delete; 

    template <typename T> 
    Log& operator<<(T const& obj) 
    { 
     if (myDest != nullptr) { 
      myDest->myStream << obj; 
     } 
    } 
}; 

(如果你的編譯器不具有移動語義,你必須 假不知何故如果出現最壞的情況,你。可以只讓登錄可變的 單一指針成員,並把相同的代碼在 一個拷貝構造函數與傳統的簽名。醜,但作爲 一個變通......)

在該溶液中,你將有一個功能log,它返回 這個類的一個實例,用一個有效的LogData (動態分配),或一個空指針,這取決於 測井是活動的還是不。 (這是可能避免的動態 分配,使用LogData具有 功能,啓動日誌記錄,並結束它的靜態實例,但它是 有點複雜。)

+0

這有點酷,但是真的有一個好處,就是'stringstream'消息,然後調用底層'ostream'上的'operator <<'?另外,'unique_lock'的移動鎖定了互斥鎖嗎?否則,我認爲你實際上並沒有獲得鎖定,對吧?或者你傳入一個已經鎖定的'unique_lock'?這裏一點困惑。 – thokra

+0

@thokra與使用ostringstream相比,有兩個重要的好處:第一個是如果日誌記錄不活躍,則不會發生格式化;第二個是我實際使用特殊的'streambuf',它有特殊的功能來注入代碼在每行的開始,並確保刷新的序列以「\ n」結尾。我也使用一個宏來調用'log',所以我可以自動傳遞'__FILE__'和'__LINE__'。而且我經常使用同一個streambuf對象,所以它的緩衝區很快達到了最大值,並且沒有更多的分配。 –

+0

該鎖在函數'log'中獲得,該函數返回'Log',並且在構建時必須將其移動到'LogData'中。 (在我最初的代碼中,'log'函數在構建'LogData'之前有幾件事要做,而且我當時沒有線程本地存儲,所以可以選擇在線程本地存儲中使用streambuf ,並且只在沖洗期間鎖定,對我來說不可用。 –