你可以在并發(fā)代碼中發(fā)現(xiàn)各式各樣的錯誤,這些錯誤不會集中于某個方面。不過,有一些錯誤與使用并發(fā)直接相關(guān),本章重點關(guān)注這些錯誤。通常,并發(fā)相關(guān)的錯誤通常有兩大類:
不必要阻塞
這兩大類的顆粒度很大,讓我們將其分成顆粒度較小的問題。
“不必要阻塞”是什么意思?一個線程被阻塞的時候,不能處理任何任務,因為它在等待其他“條件”的達成。通常這些“條件”就是一個互斥量、一個條件變量或一個future,也可能是一個I/O操作。這是多線程代碼的先天特性,不過這也不是在任何時候都可取的——衍生成“不必要阻塞”。你會問:為什么不需要阻塞?通常,是因為其他線程在等待該阻塞線程上的某些操作完成,如果該線程阻塞了,那那些線程必然會被阻塞。
這個主題可以分成以下幾個問題:
死鎖——如你在第3章所見,在死鎖的情況下,兩個線程會互相等待。當線程產(chǎn)生死鎖,應該完成的任務就會持續(xù)擱置。舉個例子來說,一些線程是負責對用戶界面操作的線程,在死鎖的情況下,用戶界面就會無響應。在另一些例子中,界面接口會保持響應,不過有些任務就無法完成,比如:查詢無結(jié)果返回,或文檔未打印。
活鎖——與死鎖的情況類似。不同的地方在于線程不是阻塞等待,而是在循環(huán)中持續(xù)檢查,例如:自旋鎖。一些比較嚴重的情況下,其表現(xiàn)和死鎖一樣(應用不會做任何處理,停止響應),CPU的使用率還居高不下;因為線程還在循環(huán)中被檢查,而不是阻塞等待。在一些不太嚴重的情況下,因為使用隨機調(diào)度,活鎖的問題還是可以解決的。
簡單的介紹了一下“不必要阻塞”的組成。
那么,條件競爭呢?
條件競爭在多線程代碼中很常見——很多條件競爭表現(xiàn)為死鎖與活鎖。而且,并非所有條件競爭都是惡性的——對獨立線程相關(guān)操作的調(diào)度,決定了條件競爭發(fā)生的時間。很多條件競爭是良性的,比如:哪一個線程去處理任務隊列中的下一個任務。不過,很多并發(fā)錯誤的引起也是因為條件競爭。
特別是,條件競爭經(jīng)常會產(chǎn)生以下幾種類型的錯誤:
數(shù)據(jù)競爭——因為未同步訪問一塊共享內(nèi)存,將會導致代碼產(chǎn)生未定義行為。在第5章已經(jīng)介紹了數(shù)據(jù)競爭,也了解了C++
的內(nèi)存模型。數(shù)據(jù)競爭通常發(fā)生在錯誤的使用原子操作,做同步線程的時候,或沒使用互斥量所保護的共享數(shù)據(jù)的時候。
破壞不變量——主要表現(xiàn)為懸空指針(因為其他線程已經(jīng)將要訪問的數(shù)據(jù)刪除了),隨機存儲錯誤(因為局部更新,導致線程讀取了不一樣的數(shù)據(jù)),以及雙重釋放(比如:當兩個線程對同一個隊列同時執(zhí)行pop操作,想要刪除同一個關(guān)聯(lián)數(shù)據(jù)),等等。不變量被破壞可以看作為“基于數(shù)據(jù)”的問題。當獨立線程需要以一定順序執(zhí)行某些操作時,錯誤的同步會導致條件競爭,比如:順序被破壞。
惡性條件競爭就如同一個殺手。死鎖和活鎖會表現(xiàn)為:應用掛起和反應遲鈍,或超長時間完成任務。當一個線程產(chǎn)生死鎖或活鎖,可以用調(diào)試器附著到該線程上進行調(diào)試。條件競爭,破壞不變量,以及生命周期問題,其表現(xiàn)都是代碼可見的(比如,隨機崩潰或錯誤輸出)——可能重寫了系統(tǒng)部分的內(nèi)存使用方式(不會改太多)。其中,可能是因為執(zhí)行時間,導致問題無法定位到具體的位置。這是共享內(nèi)存系統(tǒng)的詛咒——需要通過線程嘗試限制可訪問的數(shù)據(jù),并且還要正確的使用同步,應用中的任何線程都可以復寫(可被其他線程訪問的)數(shù)據(jù)。
現(xiàn)在已經(jīng)了解了這兩大類中都有哪些具體問題了。
下面就讓我們來了解,如何在你的代碼中定位和修復這些問題。