std::thread類代表了一個可執行的線程,它來自頭文件<thread>。與其它創建線程的API(比如 Windows API中的CreateThread)不同的是, 它可以使用普通函數、lambda函數以及仿函數(實現了operator()函數的類)。另外,它還允許向線程函數傳遞任意數量的參數。
在上面的例子中,t是一個線程對象,函數func()運行於該線程之中。調用join函數後,該調用線程(本例中指的就是主線程)就會在join進來進行執行的線程t結束執行之前,一直處於阻塞狀態。如果該線程函數執行結束後返回了一個值,該值也將被忽略。不過,該函數可以接受任意數量的參數。
|
fbm
|
儘管我們可以向線程函數傳遞任意數量的參數,但是,所有的參數都是按值傳遞的。如果需要將參數按引用進行傳遞,那麼就一定要象下例所示一樣,把該參數封裝到
std::ref或者std::cref之中。
上面程序打印結果爲43,但要不是將a封裝到std::ref之中的話,輸出的將是42。 除join方法之外,這個線程類還提供了另外幾個方法:
|
fbm
|
有一點非常重要,值得注意:線程函數中要是拋出了異常的話,使用通常的try-catch方式是捕獲不到該異常的。換句話說,下面這種做法行不通:
要在線程間傳遞異常,你可以先在線程函數中捕獲它們,然後再將它們保存到一個合適的地方,隨後再讓另外一個線程從這個地方取得這些異常。
要獲得更多關於捕獲並傳遞異常的知識,你可以閱讀在主線程中處理工作線程拋出的C++異常以及怎樣才能在線程間傳遞異常?。 在深入討論之前還有一點值得注意,頭文件<thread>裏還在命名空間std::this_thread中提供了一些輔助函數:
|
fbm
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
其它翻譯版本(1) |
鎖在上一個例子中,我需要對g_exceptions這個vector進行同步訪問,以確保同一個時刻只能有一個線程向其中壓入新元素。爲了實現同步,我使用了一個互斥量,並在該互斥量上進行了鎖定。互斥量是一個核心的同步原語,C++11的<mutex>頭文件中包含了四種不同的互斥量。
以下所列就是一個使用std::mutex(注意其中get_id()和sleep_for()這兩個前文所述的輔助函數的用法)的例子。
其輸出將類似如下所示:
lock()和unlock()這兩個方法顧名思義,頭一個方法用來對互斥量進行加鎖,如果互斥量不可得便會處於阻塞狀態;第二個方法用來對互斥量進行解鎖。 |
fbm
|
接下來的這個例子演示的是一個簡單的線程安全的容器(內部使用的是std::vector)。這個容器具有添加單個元素的add()方法以及添加一批元素的addrange()方法,addrange()方法內只是簡單的調用了add()方法。
這個程序執行起來會進入死鎖狀態。其原因在於,該容器多次嘗試獲取同一個互斥量而之前卻並沒有釋放該互斥量,這麼做是行不通的。這正是std::recursive_mutex的用武之地,它允許同一個線程多次獲得同一個互斥量,可重複獲得的最大次數並未具體說明,但一旦查過一定次數,再對lock進行調用就會拋出std::system錯誤。爲了修復上面所列代碼的死鎖問題(不通過修改addrange方法的實現,讓它不對lock和unlock方法進行調用),我們可以將互斥量改爲std::recursive_mutex。
經過修改之後,該程序的輸出會同如下所示類似:
明眼的讀者可能已經發現了,每次調用func()所產生的數字序列都完全相同。這是因爲對srad的初始化是要分線程進行的,對srand()的調用只是在主線程中進行了初始化。在其它的工作線程中,srand並沒有得到初始化,所以每次產生的數字序列就是完全相同的了。 |
fbm
|
顯式的加鎖和解鎖可能會導致一定的問題,比如忘了解鎖或者加鎖的順序不對都有可能導致死鎖。本標準提供了幾個類和函數用於幫助解決這類問題。使用這些封裝類就能夠以相互一致的、RAII風格的方式使用互斥量了,它們可以在相應的代碼塊的範圍內進行自動的加鎖和解鎖動作。這些封裝類包括:
使用這些封裝類,我們可以象這樣來改寫我們的容器:
有人會說,既然dump()方法並不會對容器的狀態做出任何修改,所以它應該定義爲congst的方法。但要是你真的這麼改了之後,編譯器就會報告出如下的錯誤: ‘std::lock_guard<_Mutex>::lock_guard(_Mutex &)' : cannot convert parameter 1 from ‘const std::recursive_mutex' to ‘std::recursive_mutex &' |
fbm
|
互斥量(無論使用的是哪一種實現)必須要獲得和釋放,這就意味着要調用非常量型的lock()和unlock()方法。所以,從邏輯上講,lock_guard不能在定義中添加const(因爲該方法定義爲const的話,互斥量也就必需是const的了)這個問題有個解決辦法,可以讓mutex變爲mutable的。成爲 mutable之後就可以在const函數中對狀態進行修改了。不過,這種用法應該只用於隱藏的或者“元”狀態(比如,對計算結果或者查詢到的數據進行緩存,以供下次調用時直接使用而無需再次計算或查詢;再比如,對
只是對對象的實際狀態起着輔助作用的互斥量中的位進行修改)。
這些封裝類都具有可以接受一個用來指導加鎖策略的參數的構造器,可用的加鎖策略有:
這些策略的定義如下所示:
|
fbm
|
除了這些互斥量的封裝類,本標準還提供了幾個用來對一個或多個互斥量進行加鎖的方法。
這裏舉一個造成死鎖的例子:我們有一個保存元素的容器,還有一個叫做exchange()的方法,用來將一個元素從一個容器中取出來放入另外一個容器。爲了成爲線程安全的函數,這個函數通過獲得每個容器的互斥量,對兩個容器的訪問進行了同步處理。
假設這個函數是從兩個不同的線程中進行調用的,在第一個線程中有一個元素從第一個容器中取出來,放到了第二個容器中,在第二個線程中該元素又從第二個容器中取出來放回到了第一個容器中。這樣會導致死鎖(如果線程上下文正好在獲得第一個鎖的時候從一個線程切換到了另一個線程的時候就會發生死鎖)。
要解決該問題,你可以使用以能夠避免死鎖的方式獲得鎖的std::lock:
|
fbm
|
條件變量C++11還提供了對另外一個同步原語的支持,這個原語就是條件變量。使用條件變量可以將一個或多個線程進入阻塞狀態,直到收到另外一個線程的通知,或者超時或者發生了虛假喚醒,才能退出阻塞狀態。頭文件<condition_variable>中包含的條件變量有兩種實現:
|
fbm
|
下面說說條件變量的工作原理:
|
fbm
|
以下代碼給出了一個利用狀態變量來同步線程的例子:幾個工作線程可能在他們運行的時候產生錯誤並且他們把這些錯誤放到隊列裏面。一個記錄線程會通過從隊列得到並輸出錯誤來處理這些錯誤代碼。當有錯誤發生的時候,工作線程會發信號給記錄線程。記錄線程一直在等待着狀態變量接收信號。爲了防止虛假的喚醒,所以記錄線程的等待是發生在一個以檢測布爾值(boolean)的循環之中的。
1.一個是隻有一個唯一鎖;這個重載釋放鎖,封鎖線程和把線程加入都是等待這一個狀態變量的線程隊列裏面;當狀態變量被信號通知後或者是一個假喚醒發生,這些線程就會被喚醒。但他們中任何一個發生時,鎖就被重新獲得然後函數返回。 2.另外一個是對於唯一鎖的添加,它也是使用一個循環的謂語直到它返回false;這個重載可以用來防止假式喚醒。它基本上是與以下是等價的:
|
張德恆
|
因此在上例中,通過使用重載的wait函數以及一個驗證隊列狀態(空或不空)的斷言,就可以避免使用布爾變量g_notified了:
除了這個wait()重載方法,還有另外兩個進行類似重載的等待方法,都有用了一個用來避免虛假喚醒的斷言:
這兩個函數不帶斷言的重載函數會返回一個cv_status狀態,該狀態用來表明線程被喚醒了到底是因爲發生了超時還是因爲條件變量收到了信號抑或是發生了虛假喚醒。 |
fbm
|
本標準還提供了一個叫做notified_all_at_thread_exit的函數,它實現了一種機制,在該機制下,我們可以通知其它線程,某個給定的線程執行結束了,並銷燬了所有的thread_local對象。之所以引入該函數,是因爲如果使用了thread_local後,採用join()之外的機制等待線程可能會導致不正確甚至是致命的行爲,出現這樣的問題是因爲thread_local的析構函數甚至可能會在原本處於等待中的線程繼續執行後被執行了而且還可能已經執行完成了。(有關這方面更多的情況可參見N3070和N2880)。 一般情況下,notified_all_at_thread_exitTypically必須正好在線程生成前調用。下面給出一個例子,演示一下notify_all_at_thread_exit是如何同condition_variable一起使用來對兩個線程進行同步處理的:
如果工作線程是在主線程結束之前結束的,輸出將會是如下所示:
如果是主線程在工作線程結束之前結束的,輸出將會是如下所示:
|
fbm
|
結束語C++11標準使得C++開發人員能夠以一種標準的和平臺獨立的方式來編寫多線程代碼。本文一一講述了標準所支持的線程和同步機制。<thread>頭文件提供了名爲thread的類(另外還包含了一些輔助類或方法),該類代表了一個執行線程。頭文件<mutex>提供了幾種互斥量的實現,以及對線程進行同步訪問的封裝類。頭文件<condition_variable>爲條件變量提供了兩種實現,利用這些實現可以讓一個或多個線程進入阻塞狀態,直到從收到來自另外一個或多個線程的通知、或者發生超時或虛假喚醒爲止纔會被喚醒。推薦在這方面再閱讀一些別的資料來獲得更詳細的信息。 |