2010-04-28 130 views
17

我目前已經創建了一個線程安全整數的C++類,它只是簡單地存儲一個整數私有並公開獲得一組函數,它使用boost :: mutex來確保一次只能有一個更改應用於整數。C++線程安全整數

這是最有效的方式嗎,我已經被告知互斥體是相當資源密集的?這個類被使用了很多,非常迅速,所以它可能是一個瓶頸...

Googleing C++線程安全整數返回不同架構上整數運算的線程安全性的不清楚的視圖和oppinions。有人說32位的32位int是安全的,但32位的64位不是由於'對齊'造成的。其他人說它是編譯器/操作系統特定的(我不懷疑)。

我在32位機器上使用Ubuntu 9.10,有些機器有雙核,所以在某些情況下,可能會在不同內核上同時執行線程,我正在使用GCC 4.4的g ++編譯器。

在此先感謝...

請注意:我已經標記爲答案「正確」是最適合我的問題 - 但也有在其他的答案做了一些優秀的點,他們是所有值得一讀!

+0

恕我直言,原子操作是可能的,http://stackoverflow.com/questions/930897/c-atomic-operations-for-lock-free - 結構可能會有所幫助。 – 2010-04-28 13:02:04

回答

6

它不是編譯器和操作系統特定的,它是特定於體系結構的。編譯器和操作系統進入它,因爲它們是你工作的工具,但它們不是那些設定真正規則的工具。這就是C++標準不會觸及問題的原因。

我從來沒有聽說過一個64位的整數寫入,它可以分成兩個32位的寫入,中途被中斷。 (是的,這是邀請其他人發佈反例。)具體來說,我從來沒有聽說過CPU的加載/存儲單元允許中斷未對齊的寫入;一箇中斷源必須等待整個錯位訪問完成。

要有一個可中斷的加載/存儲單元,其狀態必須保存到堆棧中,並且加載/存儲單元將剩餘的CPU狀態保存到堆棧中。這將是巨大的複雜,並且容易出錯,如果加載/存儲單元是可中斷的......並且您將獲得的所有內容是一個週期響應中斷的更少的等待時間,其充其量是以十的週期。完全不值得。

早在1997年,一位同事和我寫了一個C++ Queue模板,用於多處理系統。 (每個處理器都有自己的操作系統運行,以及它自己的本地內存,所以這些隊列只用於處理器之間共享的內存。)我們設計了一種通過單個整數寫入來改變隊列狀態的方法,並將此寫入爲原子操作。此外,我們要求隊列的每一端(即讀或寫索引)都由一個且只有一個處理器擁有。十三年後,代碼仍然運行良好,我們甚至有一個處理多個閱讀器的版本。

但是,如果您想將64位整數寫入原子,請將該字段與64位邊界對齊。爲什麼要擔心?

編輯:對於你在你的評論中提到的情況,我需要更多的信息來確定,所以讓我舉一個例子,可以實現沒有專門的同步代碼的東西。

假設你有N個作者和一個閱讀器。你希望作家能夠向讀者發信號。事件本身沒有數據;你只是想要一個事件計數,真的。

聲明共享內存結構,所有的作家和讀者之間共享:(使此一類或模板或您認爲合適的任何)

#include <stdint.h> 
struct FlagTable 
{ uint32_t flag[NWriters]; 
}; 

每一個作家需要被告知其索引並給予一個指向該表:

class Writer 
{public: 
    Writer(FlagTable* flags_, size_t index_): flags(flags_), index(index_) {} 
    void SignalEvent(uint32_t eventCount = 1); 
private: 
    FlagTable* flags; 
    size_t index; 
} 

當筆者想要信號的事件(或幾個),更新其標誌:

void Writer::SignalEvent(uint32_t eventCount) 
{ // Effectively atomic: only one writer modifies this value, and 
    // the state changes when the incremented value is written out. 
    flags->flag[index] += eventCount; 
} 

讀者保持它已經看到的所有標誌值的本地副本:

class Reader 
{public: 
    Reader(FlagTable* flags_): flags(flags_) 
    { for(size_t i = 0; i < NWriters; ++i) 
      seenFlags[i] = flags->flag[i]; 
    } 
    bool AnyEvents(void); 
    uint32_t CountEvents(int writerIndex); 
private: 
    FlagTable* flags; 
    uint32_t seenFlags[NWriters]; 
} 

要找出是否有任何事件發生,它只是看起來的變化值:

bool Reader::AnyEvents(void) 
{ for(size_t i = 0; i < NWriters; ++i) 
     if(seenFlags[i] != flags->flag[i]) 
      return true; 
    return false; 
} 

如果發生了什麼事情,我們可以檢查每個來源並獲得事件數:

uint32_t Reader::CountEvents(int writerIndex) 
{ // Only read a flag once per function call. If you read it twice, 
    // it may change between reads and then funny stuff happens. 
    uint32_t newFlag = flags->flag[i]; 
    // Our local copy, though, we can mess with all we want since there 
    // is only one reader. 
    uint32_t oldFlag = seenFlags[i]; 
    // Next line atomically changes Reader state, marking the events as counted. 
    seenFlags[i] = newFlag; 
    return newFlag - oldFlag; 
} 

現在這一切都陷入了困境?它是非阻塞的,也就是說,在Writer寫入內容之前,您無法讓Reader進入睡眠狀態。讀者必須選擇坐在等待AnyEvents()的旋轉循環中以返回true,這可以最大限度地減少延遲,或者每次都可以睡一會兒,這樣可以節省CPU但可以讓很多事件累積起來。所以總比沒有好,但這不是一切的解決方案。

使用實際的同步原語,人們只需要用一個互斥和條件變量來包裝這個代碼,使它正確地阻塞:讀者會睡覺,直到有事情要做。由於您對標誌使用了原子操作,因此實際上可以將互斥鎖的鎖定時間保持在最小值:Writer只需將互斥鎖鎖定足夠長的時間以發送條件,而不設置標誌,讀取器只需要在調用AnyEvents()之前等待條件(基本上,它就像上面的睡眠環路情況,但是具有等待條件而不是睡眠呼叫)。

+1

我目前使用的是32位整數,但是您能否詳細說明「將字段與64位邊界對齊」的含義。 不知道這是否澄清我的需求,但:有多個線程寫(遞增)和一個閱讀器。 – 2010-04-28 13:45:33

+0

如果你有一個64位的整數,那麼你想要將它與64位的邊界對齊,即一個可以被sizeof(uint64_t)'(或者你想要對齊的任何東西)均勻整除的字節地址。有關更多詳細信息,請參閱http://en.wikipedia.org/wiki/Data_structure_alignment。 – 2010-04-28 23:12:47

+0

甜...我擴大了我的答案,並沒有解釋downvoted。 – 2010-04-29 03:13:28

4

C++沒有真正的原子整數實現,大多數通用庫都沒有。

考慮到即使所述實現存在,也必須依賴某種互斥體 - 這是因爲您無法保證跨所有體系結構的原子操作。

7

有C++ 0x原子庫,還有一個Boost.Atomic庫正在開發中使用無鎖技術。

4

當您使用GCC時,根據您希望對整數執行的操作,您可能會忽略GCC's atomic builtins

這些可能比互斥體快一點,但在某些情況下仍比「正常」操作慢很多。

2

對於完整的通用同步,正如其他人已經提到的那樣,傳統的同步工具是非常必要的。但是,對於某些特殊情況,可以利用硬件優化。具體而言,大多數現代CPU支持整數遞減原子增量&。 GLib庫對此有非常好的跨平臺支持。本質上,庫包裝CPU &這些操作的編譯器特定彙編代碼,並默認爲互斥保護,因爲它們不可用。這當然不是非常通用的,但如果你只想維護一個櫃檯,這可能就足夠了。