前言
多執行緒、單執行緒、程式、任務、執行緒池...等等一些術語到底是什麼意思呢?到底什麼是多執行緒?它到底怎麼用?我們一起來學習一下多執行緒的處理
如何理解
程式:程式是給定程式當前正在執行的例項(作業系統的一個基本功能就是管理程式)
執行緒:執行緒是程式的一個實體,是CPU排程和分派的基本單位,它是比程式更小的能獨立執行的基本單位
單執行緒程式是僅包含一個執行緒的程式。多執行緒程式的程式則包含兩個或更多的執行緒
執行緒安全:在多執行緒程式中執行時具有正確的表現,就說程式碼是執行緒安全的
任務:任務是可能有高延遲的工作單元,目的是生成一個結果值,或者產生想要的效果
執行緒池:執行緒池是多個執行緒的集合,也是決定如何向執行緒分配工作的邏輯
多執行緒處理的目的和方式
多執行緒處理主要用於兩個方面:
1、實現多工
2、解決延遲
其中主要還是解決延遲問題:例如匯入一個大檔案的時候需要較長的時間,為了允許使用者隨時點選取消,開發者建立一個額外的執行緒來執行匯入,這樣就可以隨時點選取消,而不是直接凍結UI直至匯入完成。
當然,如果有足夠的核心使得每一個執行緒都能分配到一個核心的話,那麼每個執行緒就都使用自己各自的CPU。但是如今雖然有了多核機器,但是執行緒數任然大於核心的數量。
為了解決這一粥(CPU核心)少僧(執行緒)多的矛盾,作業系統通過稱為時間分片的機制來模擬多個執行緒併發執行。作業系統以極快的速度從一個執行緒切換到另一個執行緒,給人的感覺就是所有的執行緒都在同時執行
時間片:處理器在切換到下一個執行緒之前,執行一個特定的執行緒的時間週期稱之為時間片或量子
上下文切換:在一個給定的核心中改換執行執行緒的動作稱為上下文切換
不管是真正的多核並行執行還是使用時間分片的機制來模擬,我們說“一起”進行的兩個操作是併發的。並行程式設計是指將一個問題分解成較小的部分,並非同步的發起對每個部分的處理,使它們能併發地得到處理。
其中我們也需要考慮的是效能問題,不要產生一種誤導就是多執行緒的程式碼會更快,多執行緒知識解決處理器受限的問題。同時我們需要注意效能問題
多執行緒處理遇到的問題
寫一個多執行緒程式既複雜又困難,因為在單執行緒程式中許多成立的假設在多執行緒中變得不成立了,其中包括原子性、競態條件、複雜的記憶體模型以及死鎖
1、大多數操作不是原子性的
int Balance=10; int Money=6; if(Balance>Money) { Balance-=Money; }
在這段程式碼中,如果出現兩個執行緒都拿到Balance(當前餘額)並且都進入了if中,第一個拿走了Money(取走的金額),然後第二個沒有經過驗證繼續執行了Balance-=Money的操作,最後得出的結果是Balance剩下-2。這就導致了出現錯誤。
2、競態條件造成的不確定性
什麼是競態條件
官方的定義是如果程式執行順序的改變會影響最終結果,這就是一個競態條件(race condition).
Runnable r1 = () -> { // do something }; Runnable1 r2 = () -> { // do another thing }; Thread producer = new Thread(new ThreadStart(Runnable)); Thread producer1 = new Thread(new ThreadStart(Runnable1)); producer.Start(); producer1.Start();
兩個執行緒同時把一個類的靜態成員做50詞自增加1的操作,即
SomeClass.someMember++;
寫在兩個執行緒中,都執行50次,執行結束以後用主執行緒去取這個變數的值幾乎不可能是100. 有的時候是97,有的時候是98,這是用來說明競態條件的最有效例子。
3、記憶體模型的複雜性
假設兩個執行緒在兩個不同的程式中執行,但要訪問同一個物件中的欄位,目前的處理器不會每次都去訪問主記憶體,相反訪問的是處理的“快取記憶體”中生成的一個本地副本,這個快取會定時的與主記憶體同步,這就意味著這兩個不同程式中的執行緒以為自己讀取到的是相同的位置,實際讀取到的不是那個欄位實時更新的,造成兩個執行緒獲取的欄位結果不一致。
4、鎖定造成死鎖
當然肯定有辦法解決非原子性,防止競態條件,並且確保處理器的快取記憶體在必要時進行同步的。解決這些問題的主要機制是lock語句,這個語句就是將一部分程式碼設定為“關鍵”程式碼,一次只有一個執行緒能執行它,如果多個執行緒需要訪問它,作業系統只允許進入一個,其他的將被掛起。
當然鎖也有問題,加入不同的執行緒以不同的順序獲取鎖,就可能造成死鎖,這樣的結果就是你等著我釋放鎖,我等著你釋放鎖。此時只有對方釋放了鎖之後才能繼續執行,執行緒阻塞,造成了這段程式碼的徹底死鎖
既然鎖可以解決前三個問題,但是可能會出現死鎖的問題。那麼我們改如何避免或解決死鎖的問題呢?
如何避免死鎖
既然加入不同的執行緒以不同的順序獲取鎖可能造成死鎖,那麼我們只有確保所有的執行緒都是按照相同的順序獲得鎖,那麼死鎖就不會發生。
class Program { private static object objA = new object(); private static object objB = new object(); static void Main() { Program a = new Program(); Thread th = new Thread(new ThreadStart(a.Lock1)); th.Start(); lock (objB) { Console.WriteLine("我是objB,想獲取objA"); lock (objA) { Console.WriteLine("死鎖了"); } } Console.WriteLine("死鎖了"); Console.WriteLine(); } public void Lock1() { lock (objA) { Thread.Sleep(500); Console.WriteLine("我是objA,想獲取objB"); lock (objB) { Console.WriteLine("死鎖了"); } } } }
上面是獲取鎖的順序不恰當而導致死鎖的例子。如果把其中一個獲取鎖的位置改變一下就不會造成死鎖了,例如在Lock1中先獲取objB再獲取objA的話就不會造成死鎖了。所有我們平時在使用lock時一定得確保鎖的順序,不然很容易造成死鎖的。