2012-01-05 38 views
20

的std ::原子被C引入的新功能+ + 11,但我無法找到如何正確地使用它很多的教程。那麼下面的做法是否通用和有效?我用如何使用std ::原子有效

的一個做法是,我們有一個緩衝,我想CAS的一些字節,所以我所做的就是:

uint8_t *buf = .... 
auto ptr = reinterpret_cast<std::atomic<uint8_t>*>(&buf[index]); 
uint8_t oldValue, newValue; 
do { 
    oldValue = ptr->load(); 
    // Do some computation and calculate the newValue; 
    newValue = f(oldValue); 
} while (!ptr->compare_exchange_strong(oldValue, newValue)); 

所以我的問題是:

  1. 上面的代碼的用途醜陋的reinterpret_cast,並且這是檢索參考位置的原子指針的正確方法& buf [index]?
  2. 是一個機器字的單個字節比CAS顯著慢CAS,所以我應該避免使用它?我的代碼看起來更復雜,如果我改變它來加載一個單詞,提取字節,計算並設置新值的字節,並做CAS。這使代碼更加複雜,我也需要自己處理地址對齊問題。

編輯:如果這些問題是處理器/架構依賴,那麼x86/x64處理器的結論是什麼?

+1

C++ Concurrency in Action [(early access)](http://www.manning.com/williams/),[(amazon)](http://www.amazon.com/gp/product/1933988770/) REF = as_li_qf_sp_asin_tl?ie = UTF8&tag = gummadoon-20&linkCode = as2&camp = 1789&creative = 9325&creativeASIN = 1933988770)可能是目前這個話題的最好的書,或者說更確切的說是。 – Cubbi 2012-01-05 20:37:46

+1

關於原子的教程並不多,因爲除了像原子標誌這樣的一些簡單情況外,它是一個雷區。看「拆彈機」應該是使用原子的先決條件。使用鎖! – 2012-01-05 23:51:07

回答

23
  1. reinterpret_cast將產生未定義的行爲。您的變量是std::atomic<uint8_t>或簡單的uint8_t;你不能在他們之間施放。例如,尺寸和對齊要求可能不同。例如某些平臺僅對字提供原子操作,因此std::atomic<uint8_t>將使用完整的機器字,其中普通的uint8_t只能使用一個字節。非原子操作也可以通過各種方式進行優化,包括對周圍操作進行重新排序,並與可以提高性能的相鄰內存位置上的其他操作結合使用。

    這確實意味着如果你想對一些數據進行原子操作,那麼你必須事先知道,並創建合適的對象,而不是僅僅分配一個通用緩衝區。當然,您可以分配一個緩衝區,然後使用放置new在該緩衝區中初始化您的原子變量,但是您必須確保大小和對齊方式正確,並且您將無法使用非原子操作那個對象。

    如果你真的不在乎你的原子對象的排序約束,那麼使用memory_order_relaxed來處理那些本來是非原子操作的東西。但是,請注意這是高度專業化的,需要非常小心。例如,寫入不同的變量可能會被其他線程以不同於它們寫入的順序讀取,並且不同的線程可能以不同的順序讀取對方的值,即使在程序的相同執行中也是如此。

  2. 如果CAS是不是一個字一個字節的速度較慢,您可以使用std::atomic<unsigned>更好,但是這將有空間點球,你當然不能只用std::atomic<unsigned>訪問原始字節序列---對該數據的所有操作必須通過相同的std::atomic<unsigned>對象。你通常會更好地編寫符合你需要的代碼,並讓編譯器找出最好的方法來做到這一點。

對於x86/x64操作系統,具有std::atomic<unsigned>變量aa.load(std::memory_order_acquire)a.store(new_value,std::memory_order_release)不比負載和存儲更昂貴的非原子變量儘可能的實際指令去,但他們限制了編譯器優化。如果使用默認的std::memory_order_seq_cst,那麼這些操作中的一個或兩個操作將會導致LOCK ed指令或柵欄的同步開銷(my implementation將價格放在商店中,但其他實現可能會有不同的選擇)。但是,由於他們所施加的「單一總排序」限制,操作更容易推理。

在許多情況下,使用鎖而不是原子操作的速度一樣快,而且容易出錯。如果由於爭用導致互斥鎖的開銷很大,那麼您可能需要重新考慮您的數據訪問模式---無論如何,緩存ping pong可能會很好地擊中你。

2

reinterpret_cast<std::atomic<uint8_t>*>(...)是最definatly不檢索原子,甚至不存在保證工作的正確方法。這是因爲std::atomic<T>不能保證具有與T相同的尺寸。

您關於CAS是爲字節,然後機器字較慢的第二個問題:這真的取決於機器,它可能會更快,它可能會比較慢,或者有可能根本不存在CAS對你的目標體系字節。在後面的例子中,實現很可能需要爲原子使用一個鎖定實現或者在內部使用一個不同的(更大的)類型(這是一個與底層類型不一樣大小的原子的例子)。

從我看到的確實沒有辦法獲得現有值的std::atomic,特別是因爲它們不能保證具有相同的大小。所以你真的應該直接讓buf一個std::atomic<uint8_t>*。此外,我相對確定,即使這樣的演員將工作,通過非原子訪問到相同的地址將不能保證按預期工作(因爲這種訪問不保證是原子即使對於字節)。所以用非原子的方法來訪問你想要進行原子操作的內存位置並不合理。

注意,對於通用架構商店和字節負載原子反正,所以你沒有什麼可利用原子能公司有沒有性能開銷,只要您使用的這些操作輕鬆存儲順序。因此,如果您並不在乎某個點的執行順序(例如,因爲該程序還沒有多線程),只需使用a.store(0, std::memory_order_relaxed)而不是a.store(0)即可。

當然,如果你只是在談論你的86是reinterpret_cast可能奏效,但你的表現的問題很可能還是取決於處理器(我想,我沒有擡頭的實際指令時序爲cmpxchg)。

+0

我90%肯定字節上的原子將比一個字慢,因爲它需要做一些按位操作。我想知道它看起來會慢多少。另一件事是,我不同意你說讀/寫單個字節是原子的,至少在x86上。感謝您的建議,它使用原子數組而不是字節數組,它可以正常工作,但也會導致字節從較慢的加載,這不是我想要的。實際上,在99%的時間內,我可以告訴其他線程不會存儲到陣列中,因此不需要額外的屏障。只有很短的時間我需要做以上的事情。 – 2012-01-06 08:51:09

+0

@icando:正如我所說的它的平臺依賴。但是既然你在談論x86:爲什麼一個字節上的原子操作會比較慢?你的意思是需要做一些按位操作? X86可以本地存儲字節並具有8位'cmpxchg',所以它應該沒有關係(這不完全是,但它不應該有更多的影響,然後使用字節,而不是machinewords反正)。關於額外的障礙:這就是爲什麼我建議'memory_order_relaxed',這應該消除大部分額外的成本,因爲反正(至少在x86上)加載/存儲是原子的。 – Grizzly 2012-01-06 15:11:41

4

你的代碼肯定是錯誤的,並結合做一些有趣的事。如果事情變得非常糟糕,它可能會做你認爲它打算做的事情。我不會理解如何正確使用例如CAS但是你可以使用std::atomic<T>是這樣的:

std::atomic<uint8_t> value(0); 
uint8_t oldvalue, newvalue; 
do 
{ 
    oldvalue = value.load(); 
    newvalue = f(oldvalue); 
} 
while (!value.compare_exchange_strong(oldvalue, newvalue)); 

到目前爲止,我個人的策略是遠離任何的這種無鎖的東西拿出來留給誰知道他們在做什麼的人。我會使用atomic_flag和可能的計數器,這大概就像我一樣。從概念上講,我明白這種無鎖的東西是如何工作的,但我也明白,如果你不是非常小心的話,有太多的東西可能會出錯。

+1

我想說這是一個來自真實世界用例的問題,而不是一些學術作業。我會盡可能地遵循標準,但在現實生活中,有時我不能。 – 2012-01-06 08:56:57