回覆列表
-
1 # C語言基礎
-
2 # 小嘟嘟熊
首先你得自定義一個函式,然後建立執行緒(CreateThread),createthread中有一個引數是放你的函式名字是的,建立成功你就可以執行多執行緒了,不用了你需要結束執行緒控制代碼,
首先你得自定義一個函式,然後建立執行緒(CreateThread),createthread中有一個引數是放你的函式名字是的,建立成功你就可以執行多執行緒了,不用了你需要結束執行緒控制代碼,
大C++程式設計師可享受原生的多執行緒機制!淺析C++11多執行緒記憶體模型
前言在C++11標準中,一個重大的更新就是引入了C++多執行緒記憶體模型。本文的主要目的在於介紹C++多執行緒記憶體模型涉及到的一些原理和概念,以幫助大家理解C++多執行緒記憶體模型的作用和意義。(更多C/C++學習資料,請私信我“程式碼”,即可獲取.)
順序一致性模型(SEQUENTIAL CONSISTENCY)在介紹C++多執行緒模型之前,讓我們先介紹一下最基本的順序一致性模型。對多執行緒程式來說,最直觀,最容易被理解的執行方式就是順序一致性模型。順序一致性的提出者Lamport給出的定義是:
“… the result of any execution is the same as if the operations of all the processors were executed in some sequential order, and the operations of each individual processor appear in this sequence in the order specified by its program.”
從這個定義中我們可以看出,順序一致性主要約定了兩件事情:
(1)從單個執行緒的角度來看,每個執行緒內部的指令都是按照程式規定的順序(program order)來執行的;
(2)從整個多執行緒程式的角度來看,整個多執行緒程式的執行順序是按照某種交錯順序來執行的,且是全域性一致的;
下面我們透過一個例子來理解順序一致性。假設我們有兩個執行緒(執行緒1和執行緒2),它們分別執行在兩個CPU核上,有兩個初始值為0的全域性共享變數x和y,兩個執行緒分別執行下面兩條指令:
初始條件: x = y = 0;
因為多執行緒程式交錯執行的順序是不確定的,所以該程式可能有如下幾種執行順序:順序一致性模型的第一個約定要求每個執行緒內部的語句都是按照程式規定的順序執行,例如,執行緒1裡面的兩條語句在該執行緒中一定是x=1先執行,r1=y後執行。順序一致性的第二個約定要求多執行緒程式按照某種順序執行,且所有執行緒看見的整體執行順序必須一致,即該多執行緒程式可以按照順序1、順序2或者順序3(以及其他可能的執行順序)執行,且執行緒1和執行緒2所觀察到的整個程式的執行順序是一致的(例如,如果執行緒1“看見”整個程式的執行順序是順序 1,那麼執行緒2“看見”的整個程式的執行順序也必須是順序1,而不能是順序2或者順序3)。依照順序一致性模型,雖然這個程式還可能按其他的交錯順序執行,但是r1和r2的值卻只可能出現上面三種結果,而不可能出現r1和r2同時為0的情況。
然而,儘管順序一致性模型非常易於理解,但是它卻對CPU和編譯器的效能最佳化做出了很大的限制,所以常見的多核CPU和編譯器大都沒有實現順序一致性模型。例如,編譯器可能會為了隱藏一部分讀操作的延遲而做如下最佳化,把執行緒1中對y的讀操作(即r1=y)調換到x=1之前執行:
初始條件:x=y=0;
在這種情況下,該程式如果按下面的順序執行就可能就會出現r1和r2都為0這樣的違反順序一致性的結果:那麼為什麼編譯器會做這樣的亂序最佳化呢?因為讀一個在記憶體中而不是在cache中的共享變數需要較長的時鐘週期,所以編譯器就“自作聰明”的讓讀操作先執行,從而隱藏掉一些指令執行的延遲,從而提高程式的效能。實際上,這種最佳化是序列時代非常普遍的,因為它對單執行緒程式的語義是沒有影響的。但是在進入多核時代後,編譯器缺少語言級的記憶體模型的約束,導致其可能做出違法順序一致性規定的多執行緒語義的錯誤最佳化。同樣的,多核CPU中的寫緩衝區(store buffer)也可能實施亂序最佳化:它會把要寫入記憶體的值先在緩衝區中快取起來,以便讓該寫操作之後的指令先執行,進而出現違反順序一致性的執行順序。
因為現有的多核CPU和編譯器都沒有遵守順序一致模型,而且C/C++的現有標準中都沒有把多執行緒考慮在內,所以給編寫多執行緒程式帶來了一些問題。例如,為了正確地用C++實現Double-Checked Locking,我們需要使用非常底層的記憶體柵欄(Memory Barrier)指令來顯式地規定程式碼的記憶體順序性(memory ordering)[5]。然而,這種方案依賴於具體的硬體,因此可移植性很差;而且它過於底層,不方便使用。
C++多執行緒記憶體模型為了更容易的進行多執行緒程式設計,程式設計師希望程式能按照順序一致性模型執行;但是順序一致性對效能的損失太大了,CPU和編譯器為了提高效能就必須要做最佳化。為了在易程式設計性和效能間取得一個平衡,一個新的模型出爐了:sequential consistency for data race free programs,它就是即將到來的C++1x標準中多執行緒記憶體模型的基礎。對C++程式設計師來說,隨著C++1x標準的到來,我們終於可以依賴高階語言內建的多執行緒記憶體模型來編寫正確的、高效能的多執行緒程式。
C++記憶體模型可以被看作是C++程式和計算機系統(包括編譯器,多核CPU等可能對程式進行亂序最佳化的軟硬體)之間的契約,它規定了多個執行緒訪問同一個記憶體地址時的語義,以及某個執行緒對記憶體地址的更新何時能被其它執行緒看見。這個模型約定:沒有資料競跑的程式是遵循順序一致性的。該模型的核心思想就是由程式設計師用同步原語(例如鎖或者C++1x中新引入的atomic型別的共享變數)來保證你程式是沒有資料競跑的,這樣CPU和編譯器就會保證程式是按程式設計師所想的那樣執行的(即順序一致性)。換句話說,程式設計師只需要恰當地使用具有同步語義的指令來標記那些真正需要同步的變數和操作,就相當於告訴CPU和編譯器不要對這些標記好的同步操作和變數做違反順序一致性的最佳化,而其它未被標記的地方可以做原有的最佳化。編譯器和CPU的大部分最佳化手段都可以繼續實施,只是在同步原語處需要對最佳化做出相應的限制;而且程式設計師只需要保證正確地使用同步原語即可,因為它們最終表現出來的執行效果與順序一致性模型一致。由此,C++多執行緒記憶體模型幫助我們在易程式設計性和效能之間取得了一個平衡。
在C++11標準之前,C++是在建立在單執行緒語義上的。為了進行多執行緒程式設計,C++程式設計師透過使用諸如Pthreads,Windows Thread等C++語言標準之外的執行緒庫來完成程式碼設計。以Pthreads為例,它提供了類似pthread_mutex_lock這樣的函式來保證對共享變數的互斥訪問,以防止資料競跑。人們不禁會問,Pthreads這樣的執行緒庫我用的好好的,幹嘛需要C++引入的多執行緒,這不是多此一舉麼?其實,以執行緒庫的形式進行多執行緒程式設計在絕大多數應用場景下都是沒有問題的。然而,執行緒庫的解決方案也有其先天缺陷。第一,如果沒有在程式語言中定義記憶體模型的話,我們就不能清楚的定義到底什麼樣的編譯器/CPU最佳化是合法的,而程式設計師也不能確定程式到底會怎麼樣被最佳化執行。例如,Pthreads標準中並未對什麼是資料競跑(Data Race)做出精確定義,因此C++編譯器可能會進行一些錯誤最佳化從而導致資料競跑[3]。第二,絕大多數情況下執行緒庫能正確的完成任務,而在極少數對效能有更高要求的情況下(尤其是需要利用底層的硬體特性來實現高效能Lock Free演算法時)需要更精確的記憶體模型以規定好程式的行為。簡而言之,把記憶體模型整合到程式語言中去是比執行緒庫更好的選擇。
C++11中引入的ATOMIC型別C++作為一種高效能的系統語言,其設計目標之一就在於提供足夠底層的操作,以滿足對高效能的需求。在這個前提之下,C++11除了提供傳統的鎖、條件變數等同步機制之外,還引入了新的atomic型別。相對於傳統的mutex鎖來說,atomic型別更底層,具備更好的效能,因此能用於實現諸如Lock Free等高效能並行演算法。有了atomic型別,C++程式設計師就不需要像原來一樣使用匯編程式碼來實現高效能的多執行緒程式了。而且,把atomic型別整合到C++語言中之後,程式設計師就可以更容易地實現可移植的多執行緒程式,而不用再依賴那些平臺相關的彙編語句或者執行緒庫。對常見的資料型別,C++11都提供了與之相對應的atomic型別。以bool型別舉例,與之相對應的atomic_bool型別具備兩個新屬性:原子性與順序性。顧名思義,原子性的意思是說atomic_bool的操作都是不可分割的,原子的;而順序性則指定了對該變數的操作何時對其他執行緒可見。在C++11中,為了滿足對效能的追求,atomic型別提供了三種順序屬性:sequential consistency ordering(即順序一致性),acquire release ordering以及relaxed ordering。因為sequential consistency是最易理解的模型,所以預設情況下所有atomic型別的操作都會使sequential consistency順序。當然,順序一致性的效能相對來說比較差,所以程式設計師還可以使用對順序性要求稍弱一些的acquire release ordering與最弱的relaxed ordering。
在下面這個例子中,atomic_bool型別的變數data_ready就被用來實現兩個執行緒間的同步操作。需要注意的是,對data_ready的寫操作仍然可以透過直接使用賦值運算子(即“=”)來進行,但是對其的讀操作就必須呼叫load()函式來進行。在預設的情況下,所有atomic型別變數的順序性都是順序一致性(即sequential consistency)。在這個例子中,因為data_ready的順序性被規定為順序一致性,所以執行緒1中對data_ready的寫操作會與執行緒2中對data_ready的讀操作構建起synchronize-with的同步關係,即#2->#3。又因為writer_thread()中的程式碼順序規定了#1在#2之前發生,即#1->#2;而且reader_thread中的程式碼順序規定了#3->#4,所以就有了#1->#2->#3->#4這樣的順序關係,從而可以保證在#4中讀取data的值時,#1已經執行完畢,即#4一定能讀到#1寫入的值(10)。
相信很多朋友會納悶,這樣的執行順序不是顯然的麼?其實不然。如果我們把data_ready的順序性制定為relaxed ordering的話,編譯器和CPU就可以自由地做違反順序一致性的亂序最佳化,從而導致#1不一定在#2之前被執行,最終導致#4中讀到的data的值不為10。
簡單的來說,在atomic型別提供的三種順序屬性中,acquire release ordering對順序性的約束程度介於sequential consistency(順序一致性)和relaxed ordering之間,因為它不要求全域性一致性,但是具有synchronized with的關係。Relaxed ordering最弱,因為它對順序性不做任何要求。由此可見,除非非常必要,我們一般不建議使用relaxed ordering,因為這不能保證任何順序性。關於這三種屬性更詳細的資訊大家可以參考[1]。
透過上面的例子我們可以看到,C++1x中的多執行緒記憶體模型為了透過atomic型別提供足夠的靈活性和效能,最大限度地將底層細節(三種不同的順序屬性)暴露給了程式設計師。這樣的設計原則一方面給程式設計師提供了實現高效能多執行緒演算法的可能,但卻也大大增加了使用上的難度。我個人的建議是,如果常規的mutex鎖、條件變數、future訊號能滿足您的設計需求,那麼您完全不需要使用atomic變數。如果您決定使用atomic變數,請儘量使用預設的順序一致性屬性。
總結本文對C++11標準中新引入的多執行緒記憶體模型進行了簡要介紹。C++11多執行緒記憶體模型的引入使得廣大C++程式設計師可以享受語言原生支援的多執行緒機制,併為實現高效能多執行緒演算法提供了足夠豐富的工具(例如atomic型別)。但是,多執行緒記憶體模型本身的複雜性,以及一些底層機制(例如不同的順序性屬性)的引入也給使用C++進行多執行緒程式設計帶來了不小的複雜度。如何高效、可靠的利用好這些新引入的多執行緒機制將會成為一個新的挑戰。