multithreading thread教學 - 並發:C++11內存模型中的原子和易失性



class new (5)

全局變量在2個不同核心上的2個並發運行線程之間共享。 線程寫入和讀取變量。 對於原子變量,一個線程可以讀取陳舊值嗎? 每個核心可能在其緩存中具有共享變量的值,並且當一個線程在緩存中寫入其副本時,另一個核心上的另一個線程可能從其自己的緩存中讀取過時值。 或者編譯器執行強大的內存排序以從其他緩存中讀取最新值? c ++ 11標準庫具有std :: atomic支持。 這與volatile關鍵字有何不同? 在上述場景中,volatile和atomic類型的行為有何不同?


Answers

以下是兩件事的基本概要:

1)易失性關鍵字:
告訴編譯器這個值可能隨時改變,因此它不應該將它緩存在寄存器中。 在C中查找舊的“register”關鍵字。“Volatile”基本上是“ - ”運算符,以“註冊”的“+”。 現代編譯器現在進行“註冊”優化,默認情況下用於顯式請求,因此您只能看到“volatile”。 使用volatile限定符將保證您的處理永遠不會使用陳舊值,但僅此而已。

2)原子:
原子操作在單個時鐘週期內修改數據,因此任何其他線程都不可能在這種更新過程中訪問數據。 它們通常僅限於硬件支持的任何單時鐘組裝指令; 像++, - 和交換2個指針之類的東西。 請注意,這並沒有說明ORDER,不同的線程將運行原子指令,只是它們永遠不會並行運行。 這就是為什麼你有所有這些額外的選項來強制訂購。


首先, volatile並不意味著原子訪問。 它專為內存映射I / O和信號處理等設計。 當與std::atomic一起使用時, volatile是完全不必要的,除非您的平台文檔另有說明,否則volatile與線程之間的原子訪問或內存排序無關。

如果您有一個在線程之間共享的全局變量,例如:

std::atomic<int> ai;

然後,可見性和排序約束取決於您用於操作的內存排序參數,以及鎖,線程和對其他原子變量的訪問的同步效果。

在沒有任何額外同步的情況下,如果一個線程將值寫入ai ,則沒有任何東西可以保證另一個線程將在任何給定時間段內看到該值。 該標準規定它應該“在合理的時間段內”可見,但任何給定的訪問都可能返回陳舊的值。

std::memory_order_seq_cst的默認內存排序為所有變量中的所有std::memory_order_seq_cst操作提供單個全局總排序。 這並不意味著你不能得到陳舊的價值,但它確實意味著你所獲得的價值決定了並且取決於你的操作所處的總順序中的位置。

如果你有2個共享變量xy ,最初為零,並且有一個線程寫入1到x而另一個寫入2到y ,那麼讀取兩者的第三個線程可以看到(0,0),(1,0), (0,2)或(1,2)因為操作之間沒有排序約束,因此操作可以以全局順序的任何順序出現。

如果兩個寫入都來自同一個線程,在y=2之前x=1且讀取線程在x之前讀取y ,那麼(0,2)不再是有效選項,因為讀取y==2意味著早期寫入x是可見的。 其他3對(0,0),(1,0)和(1,2)仍然是可能的,這取決於2次讀取如何與2次寫入交錯。

如果使用其他內存排序,例如std::memory_order_relaxedstd::memory_order_acquire那麼約束將進一步放寬,並且單個全局排序不再適用。 如果沒有額外的同步,線程甚至不必同意兩個存儲的排序來分離變量。

保證具有“最新”值的唯一方法是使用read-modify-write操作,如exchange()compare_exchange_strong()fetch_add() 。 讀 - 修改 - 寫操作有一個額外的約束,它們總是對“最新”值進行操作,因此一系列線程的ai.fetch_add(1)操作序列將返回一系列沒有重複或間隙的值。 在沒有其他約束的情況下,仍然無法保證哪些線程會看到哪些值。

使用原子操作是一個複雜的主題。 我建議你閱讀很多背景材料,並在用atomics編寫生產代碼之前檢查已發布的代碼。 在大多數情況下,編寫使用鎖的代碼更容易,而且效率明顯降低。


volatile和原子操作有不同的背景,並以不同的意圖引入。

volatile日期,主要用於在訪問內存映射IO時阻止編譯器優化。 現代編譯器通常只會抑制volatile優化,儘管在某些機器上,這對於內存映射IO來說還不夠。 除了信號處理程序的特殊情況,以及setjmplongjmpgetjmp序列(其中C標準,在信號的情況下,Posix標準,提供了額外的保證),在現代機器上它必須被認為是無用的特殊的附加指令(圍欄或內存屏障),硬件可能會重新排序甚至抑制某些訪問。 因為你不應該使用setjmp等。 在C ++中,這或多或少會留下信號處理程序,而在多線程環境中,至少在Unix下,也有更好的解決方案。 並且可能是內存映射的IO,如果您正在處理內核代碼並且可以確保編譯器生成所討論的平台所需的任何內容。 (根據標準, volatile訪問是可觀察的行為,編譯器必須遵守。但編譯器可以定義“access”的含義,並且大多數似乎將其定義為“執行加載或存儲機器指令”。在現代處理器上,這甚至不意味著總線上必然存在讀取或寫入周期,更不用說按照預期的順序。)

鑑於這種情況,C ++標準增加了原子訪問,它確實提供了一定數量的線程保證; 特別地,圍繞原子訪問生成的代碼將包含必要的附加指令以防止硬件重新排序訪問,並確保訪問傳播到多核機器上的核之間共享的全局存儲器。 (在標準化工作的某一點上,微軟建議將這些語義添加到volatile ,我認為他們的一些C ++編譯器會這樣做。然而,在討論了委員會中的問題之後,包括微軟代表在內的普遍共識就是它最好留下具有原始意義的volatile ,並定義原子類型。)或者只使用系統級原語,如互斥體,它們執行代碼中需要的任何指令。 (他們必須這樣做。如果沒有關於內存訪問順序的保證,則無法實現互斥鎖。)


揮發性和原子性有不同的用途。

易失性:通知編譯器避免優化。 此關鍵字用於意外更改的變量。 因此,它可用於表示硬件狀態寄存器,ISR變量,多線程應用程序中共享的變量。

原子:它也用於多線程應用程序。 但是,這確保了在多線程應用程序中使用時沒有鎖定/停止。 原子操作沒有種族和不可分割。 使用的關鍵場景很少是檢查鎖是免費還是使用,原子地添加到值並在多線程應用程序中返回添加值等。


什麼是lambda函數?

lambda函數的C ++概念來源於lambda演算和函數式編程。 lambda是一個未命名的函數,對於不可重用且不值得命名的代碼片段很有用(在實際編程中,而非理論上)。

在C ++中,lambda函數是這樣定義的

[]() { } // barebone lambda

或者所有的榮耀

[]() mutable -> T { } // T is the return type, still lacking throw()

[]是捕獲列表, ()參數列表和{}函數體。

捕獲列表

捕獲列表定義了lambda外部應該在函數體內可用的內容以及如何實現。 它可以是:

  1. 值:[x]
  2. 參考文獻[&x]
  3. 目前在參考範圍內的任何變量[&]
  4. 與3相同,但按值[=]

你可以用逗號分隔的列表[x, &y]混合上述任何一個。

參數列表

參數列表與其他C ++函數中的參數列表相同。

功能體

實際調用lambda時將執行的代碼。

返回類型扣除

如果lambda只有一個return語句,則返回類型可以省略,並且具有隱式類型的decltype(return_statement)

易變的

如果一個lambda被標記為mutable(例如[]() mutable { } ),那麼它允許突變已被值捕獲的值。

用例

由ISO標准定義的庫很大程度上受益於lambda表達式,並提高了可用性的幾個方面,因為現在用戶不必在一些可訪問的範圍內使用小函數來混淆他們的代碼。

C ++ 14

在C ++中,14個lambda通過各種提議得到了擴展。

初始化的Lambda捕獲

捕獲列表的一個元素現在可以用=來初始化。 這允許重命名變量並通過移動來捕獲。 從標準中取得一個例子:

int x = 4;
auto y = [&r = x, x = x+1]()->int {
            r += 2;
            return x+2;
         }();  // Updates ::x to 6, and initializes y to 7.

和一個從維基百科顯示如何捕捉std::move

auto ptr = std::make_unique<int>(10); // See below for std::make_unique
auto lambda = [ptr = std::move(ptr)] {return *ptr;};

通用Lambdas

Lambdas現在可以是泛型的(如果T是周圍範圍內的某個類型模板參數, auto在這裡等於T ):

auto lambda = [](auto x, auto y) {return x + y;};

改進的退貨類型扣除

C ++ 14允許為每個函數推導返回類型,並且不會將其限制為形式return expression;函數return expression; 。 這也延伸到lambda。





c++ multithreading concurrency c++11 parallel-processing