通過前面的學習,我們學會了利用thread創建線程來執行線程函數,學會了利用函數參數向線程函數傳遞數據,也學會了利用future和promise來獲得線程函數的結果數據,似乎我們只用了20%的技術就已經完成了多線程開發中80%的任務。可是,別高興得太早,二八原則告訴我們,用來完成那剩下的20%的任務,我們不得不用上那剩下的80%的技術。
前面我們所遇到的多線程應用場景,都只是各個線程各自獨立地訪問自己的資源,各個線程之間並沒有共享資源,也就不存在共享資源的競爭。然而在更多時候,多個線程之間往往需要共享資源,比如它們都需要訪問某個共享的容器,這時就存在一個共享資源競爭的問題。當多個線程各自獨立地同時訪問某個共享資源時,將導致未定義的結果。比如,某個線程正在向某個容器插入數據的同時,另外一個線程正在從容器中讀取數據,兩個線程將在誰先完成對容器的操作上進行競爭。其最後的結果是不可預料的,有可能讀操作失敗,也可能寫操作失敗,更有可能讀寫都失敗。這種不確定的行為對總是按照條條框框做事的程序來說是絕不容許的。為此,C++11專門提出了多種措施來處理線程之間共享資源的競爭,以保證在任何時刻都只有唯一的一個線程對共享資源進行操作,從而確保其操作行為結果的確定性和唯一性。在這些措施當中,最簡單也是最常用的就是互斥機制。互斥機制主要通過mutex類來實現。我們首先在程序中創建一個全局的mutex對象,然後通過在線程函數中先後調用這個對象的lock()成員函數和unlock()成員函數來形成一個代碼區域,通常稱為臨界區。而互斥機制保證了在任何時刻,最多只能有一個線程進入到lock()函數和unlock()函數之間的臨界區執行其中的代碼。當第一個線程正在臨界區執行時,臨界區處於鎖定狀態。如果後續有執行到lock()函數臨界區開始位置的線程將會被阻塞,進入線程隊列等待,直到第一個線程執行到unlock()函數臨界區結束離開臨界區,解除了臨界區的鎖定,其他處於線程隊列中等待的線程才會按照先進先出(FIFO,First In,First Out)的規則進入臨界區執行,而一旦有線程進入臨界區,臨界區又會被再次鎖定。其他未進入臨界區的線程只有在線程隊列中繼續等待,直到它的機會到來。由於互斥對象所確定的臨界區每次只能有一個線程進入執行,如果我們將對共享資源的訪問放到臨界區來進行,這樣就能保證每次只有一個線程在臨界區對共享資源進行訪問,也就避免了共享資源被多個線程同時訪問的問題。例如,在前面的餐館中有兩個廚子,他們都會將炒好的菜交給一個服務員讓她端給客人,這裡,服務員就成了共享資源,為了避免兩個廚子同時讓服務員端菜,我們用mutex對象為她設立一個臨界區,讓她在臨界區工作:
在這裡,我們首先創建了一個全局的mutex對象m以及一個共享資源quFoods容器,然後在線程函數Cook()中,我們用m的lock()函數和unlock()函數形成了一個臨界區。因為Food對象的創建不涉及共享資源,各個線程可以各自獨立地進行,所以我們把Food對象的創建工作放在臨界區之外進行。當Food對象創建完成需要添加到quFoods容器時,就涉及到了對共享資源quFoods的操作,就需要放到臨界區來進行以保證任何時刻只有唯一的線程對quFoods進行操作。當多個線程在執行線程函數Cook()時,其流程如下圖12-6所示: