簡體   English   中英

C ++中的“偽原子”操作

[英]“pseudo-atomic” operations in C++

所以我知道C ++中沒有什么是原子的。 但我想弄清楚我是否有任何“偽原子”假設。 原因是我想避免在一些簡單的情況下使用互斥鎖,我只需要非常弱的保證。

1)假設我有全局定義的volatile bool b,最初我設置為true。 然后我啟動一個執行循環的線程

while(b) doSomething();

同時,在另一個線程中,我執行b = true。

我可以假設第一個線程將繼續執行嗎? 換句話說,如果b開始為true,並且第一個線程在第二個線程分配b = true的同時檢查b的值,我可以假設第一個線程將b的值讀為true嗎? 或者有可能在賦值的某個中間點b = true,b的值可能被讀為false?

2)現在假設b最初是假的。 然后第一個線程執行

bool b1=b;
bool b2=b;
if(b1 && !b2) bad();

而第二個線程執行b = true。 我可以假設bad()永遠不會被調用嗎?

3)int或其他內置類型怎么樣:假設我有volatile int i,最初(比如說)7,然后我指定i = 7。 我可以假設,在此操作期間的任何時間,從任何線程,i的值將等於7?

4)我有volatile int i = 7,然后我從某個線程執行i ++,所有其他線程只讀取i的值。 除了7或8之外,我可以假設我在任何線程中都沒有任何價值嗎?

5)我有一個volatile int i,從一個執行i = 7的線程,從另一個執行i = 8。 之后,我保證是7或8(或者我選擇分配的兩個值)?

標准C ++中沒有線程,並且Threads不能實現為庫

因此,該標准對使用線程的程序的行為沒有任何意義。 您必須查看線程實現提供的任何其他保證。

也就是說,在我使用的線程實現中:

(1)是的,您可以假設不相關的值不會寫入變量。 否則整個內存模型就會消失。 但要小心,當你說“另一個線程”永遠不會將b設置為false時,這意味着任何地方。 如果是這樣,那么寫入可能會在循環期間重新排序。

(2)不,編譯器可以將分配重新排序為b1和b2,因此b1可能最終為真,而b2為假。 在這么簡單的情況下,我不知道它為什么要重新排序,但在更復雜的情況下,可能有很好的理由。

[編輯:oops,當我回答時(2)我忘記了b是不穩定的。 從一個volatile變量讀取不會被重新排序,對不起,所以在典型的線程實現上是這樣的(如果有任何這樣的事情),你可以假設你最終不會以b1為真,而b2為假。

(3)與1.一般的volatile一般與線程無關。 但是,在某些實現(Windows)中它非常令人興奮,並且可能實際上意味着內存障礙。

(4)在一個體系結構中, int寫入是原子的,盡管volatile與它無關。 也可以看看...

(5)仔細檢查文檔。 可能是的,並且volatile再次無關緊要,因為在幾乎所有的體系結構中, int寫入都是原子的。 但是如果int write不是原子的,那么沒有(前一個問題沒有),即使它是不穩定的,你原則上可以獲得不同的值。 但是,鑒於這些值為7和8,我們正在討論一個非常奇怪的架構,其中包含要在兩個階段寫入的相關位,但是使用不同的值可以更合理地獲得部分寫入。

對於一個更合理的例子,假設出於一些奇怪的原因,你在一個平台上只有8位int,其中只有8位寫入是原子的。 奇怪,但合法,因為int必須至少16位,你可以看到它是如何產生的。 進一步假設你的初始值是255.那么增量可以合法地實現為:

  • 讀舊值
  • 在寄存器中遞增
  • 寫下結果的最重要字節
  • 寫下結果的最低有效字節。

一個只讀線程,它在第三步和第四步之間中斷遞增線程,可以看到值511.如果寫入是另一個順序,它可以看到0。

如果一個線程寫入255,另一個線程同時寫入256,並且寫入交錯,則永久保留不一致的值。 許多架構都不可能,但要知道這不會發生,你至少需要知道一些架構。 C ++標准中沒有任何內容禁止它,因為C ++標准談到執行被信號中斷,但是否則沒有執行的概念被程序的另一部分中斷,也沒有並發執行的概念。 這就是為什么線程不僅僅是另一個庫 - 添加線程從根本上改變了C ++執行模型。 它要求實現以不同的方式執行操作,因為您最終會發現是否例如在gcc下使用線程而忘記指定-pthreads

對齊的 int寫入是原子的平台上也可能發生同樣的情況,但是允許未對齊的int寫入而不是原子。 例如,對於x86上的IIRC,如果未對齊的int寫入超過高速緩存行邊界,則它們不保證是原子的。 由於這個原因,x86編譯器不會錯誤地對齊聲明的int變量。 但如果你玩結構包裝的游戲,你可能會引發一個例子。

所以:幾乎任何實現都會為您提供所需的保證,但可能會以相當復雜的方式完成。

一般來說,我發現不值得嘗試依賴特定於平台的內存訪問保證,我不完全理解,以避免互斥。 使用互斥鎖,如果速度太慢,請使用由真正了解架構和編譯器的人編寫的高質量無鎖結構(或實現一個設計)。 它可能是正確的,並且正確性可能會優於我自己創造的任何東西。

大多數答案都正確地解決了您將要遇到的CPU內存排序問題,但沒有一個能夠詳細說明編譯器如何通過以破壞您的假設的方式重新排序代碼來挫敗您的意圖。

考慮一下這篇文章的一個例子:

volatile int ready;       
int message[100];      

void foo(int i) 
{      
    message[i/10] = 42;      
    ready = 1;      
}

-O2及以上,最新版本的GCC和英特爾C / C ++(不了解VC ++)將首先ready存儲,因此它可以與i/10計算重疊( volatile不會為您節省!) :

    leaq    _message(%rip), %rax
    movl    $1, _ready(%rip)      ; <-- whoa Nelly!
    movq    %rsp, %rbp
    sarl    $2, %edx
    subl    %edi, %edx
    movslq  %edx,%rdx
    movl    $42, (%rax,%rdx,4)

這不是一個錯誤,它是利用CPU流水線的優化器。 如果在訪問message內容之前另一個線程正在等待ready ,那么你就會有一個令人討厭且模糊不清的競賽。

采用編譯器障礙以確保您的意圖得到尊重。 還利用86的相對強排序的例子是釋放/消耗德米特里Vyukov的單生產者單消費者隊列中找到包裝貼在這里

// load with 'consume' (data-dependent) memory ordering 
// NOTE: x86 specific, other platforms may need additional memory barriers
template<typename T> 
T load_consume(T const* addr) 
{  
  T v = *const_cast<T const volatile*>(addr); 
  __asm__ __volatile__ ("" ::: "memory"); // compiler barrier 
  return v; 
} 

// store with 'release' memory ordering 
// NOTE: x86 specific, other platforms may need additional memory barriers
template<typename T> 
void store_release(T* addr, T v) 
{ 
  __asm__ __volatile__ ("" ::: "memory"); // compiler barrier 
  *const_cast<T volatile*>(addr) = v; 
} 

我建議如果您打算進入並發內存訪問領域,請使用一個可以為您處理這些細節的庫。 雖然我們都在等待n2145std::atomic檢查Thread Building Blocks的tbb::atomic或即將推出的boost::atomic

除了正確性,這些庫可以簡化您的代碼並澄清您的意圖:

// thread 1
std::atomic<int> foo;  // or tbb::atomic, boost::atomic, etc
foo.store(1, std::memory_order_release);

// thread 2
int tmp = foo.load(std::memory_order_acquire);

使用顯式內存排序, foo的線程間關系很明確。

可能這個線程很古老,但C ++ 11標准DOES有一個線程庫,也是一個用於原子操作的龐大的原子庫。 目的是專門用於並發支持並避免數據爭用。 相關標題是原子的

依賴於此通常是一個非常非常糟糕的主意,因為你最終可能會遇到一些糟糕的事情而且只有一些架構。 最好的解決方案是使用有保證的原子API,例如Windows Interlocked api。

如果您的C ++實現提供了由n2145或其某些變體指定的原子操作庫,那么您可能會依賴它。 否則,你通常不能在語言層面依賴關於原子性的“任何東西”,因為現有的C ++標准沒有規定任何類型的多任務處理(因此處理多任務處理的原子性)。

C ++中的易失性與Java中的作用不同。 史蒂夫說,所有案件都是不明確的行為。 對於編譯器,給定的處理器體系結構和多線程系統,某些情況可能是好的,但切換優化標志可能會使您的程序行為不同,因為C ++ 03編譯器不了解線程。

C ++ 0x定義了避免競爭條件的規則以及幫助您掌握該規則的操作,但是可能知道還沒有編譯器實現與該主題相關的標准的所有部分。

我的回答令人沮喪:不,不,不,不,不。

1-4)編譯器可以使用它寫入的變量來做任何事情。 它可以在其中存儲臨時值,只要最終執行的操作與在真空中執行的線程執行相同的操作即可。 任何有效的

5)不,不保證。 如果變量不是原子的,並且您在一個線程上寫入它,並在另一個線程上讀取或寫入它,那么它就是一個競爭案例。 規范聲明這種種族案例是未定義的行為,絕對是有的。 話雖這么說,你很難找到一個不會給你7或8的編譯器,但編譯器給你別的東西是合法的。

我總是提到這種對種族案例的高度滑稽的解釋。

http://software.intel.com/en-us/blogs/2013/01/06/benign-data-races-what-c​​ould-possibly-go-wrong

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM