在當今計算機系統中,已經大量存在多核心CPU,或者是在多核心基礎上有進一步的超執行緒技術將虛擬CPU數量翻倍。在計算機發展之初,我們的應用程式是按照一個CPU只做一件事情來應用,也就是順序執行。隨著時間的不斷變化,我們的CPU計算能力越加強大,那麼我們可以使用執行緒技術,讓每個核心都去做一件事,或者使用時間切片(time slicing)技術,讓我們的CPU在各個執行緒中切換以同時達到一種處理多個執行緒任務的目標。可以同時聽歌,看文件,執行時鐘,掛遊戲。
需要注意的是,對於時間分片技術,我們實際上是同一個核心將一個執行緒的執行1個時間片(time slice)的時間,然後儲存狀態,切換到另外一個執行緒去,這種切換動作被稱為上下文切換。表現出來是並行運算。但是我們需要考慮到CPU不斷地切換執行緒,實際上也會有代價,需要儲存上一個執行緒狀態,切換到下一個執行緒去。如果執行緒數目過多的情況下,就會消耗大量時間在切換執行緒上。這就是為什麼多執行緒不一定會讓程式更快,而需要綜合衡量。
多執行緒併發問題:缺乏原子性、競態條件、複雜的記憶體模型和死鎖。
一、缺乏原子性
首先原子操作的定義是:一個原子內程式碼,要麼處於沒有操作,要麼已經操作完畢,而不存在“操作中”這個中間態。核心是它是整體的,不可分割。如果我們的多執行緒程式碼如果不是原子性的,那麼這種情況下它就缺乏原子性。
二、競態條件
競態條件是值得我們同時開啟的多個執行緒,它的執行順序是根據系統判斷哪個執行緒競爭勝利,先執行到一部分,然後發生上下文切換到另外一個另外一個執行緒去。而至於哪個執行緒在競爭中勝出不可預知,就算99.99%的時間具有正確行為,那麼也有0.01%會出現另外一個執行緒競爭勝利。
三、複雜的記憶體模型
現在我們的CPU不會每次執行某個變數的時候都會去記憶體取出操作,而是將這個變數快取在CPU的快取記憶體中,這個快取會定時和主記憶體進行同步。意味著在多核心CPU中處理不同執行緒時,我們的執行緒處理的是各自CPU核心的快取記憶體中的變數,實際上是2個不同的變數。那麼當我們多執行緒對該變數進行更新時就不是準確的。
四、鎖定造成死鎖
C#中使用Lock語句或者Monitor.Enter() Monitor.Exit()將一段程式碼作為原子操作,對Lock住的該物件,系統會判斷只允許一個執行緒訪問該段Lock住程式碼,其他執行緒掛起等待。如果被Lock住的物件發生改變,其他執行緒訪問過來的時候,系統會認為不是同一段Lock程式碼,就會允許那個程式碼訪問,這就是Lock失效了。
鎖本身也會出問題,例如此處有2個執行緒分別是A和B執行緒, 同時有2個鎖分別是C和D,那麼A執行緒在C鎖獲得之後請求D鎖,而B執行緒在獲得D鎖之後請求C鎖,就會造成互相等待對方釋放,這就是死鎖的由來。