執行緒與同步非同步

Jscroop發表於2020-05-01

執行緒與同步非同步

一、執行緒

1、什麼是執行緒?什麼是程式?兩者有什麼關係?

程式(Process):程式代表了作業系統上執行著的一個應用程式,每個程式都有自己獨立的邊界,程式與程式之間不能共享資源,一個程式可以包含一個或多個執行緒;

執行緒(Thread):執行緒是被作業系統排程的基本單元,同一程式內的所有執行緒共享記憶體和資源,並且一個執行緒可以對同一程式內的其他執行緒進行訪問或結束等操作;

關係:它們是一個包含的關係,程式就像是執行緒的容器,且至少包含一個執行緒

為了更形象的理解該部分內容,可以參考阮一峰的 程式與執行緒的一個簡單解釋


2、系統是如何呼叫執行緒的?

搶佔式排程:所有的執行緒都在被不停地快速切換執行,使得使用者感覺所有的執行緒都在並行執行;

非搶佔式排程:某個執行緒在執行時不會被作業系統強制暫停,它可以持續地執行直到執行告一段落並主動交出執行權;

通常情況下,一些系統級別的執行緒採用的是非搶佔式排程,而普通執行緒採用的是搶佔式排程


3、呼叫會不會存在問題?

對於單核CPU的作業系統來說,執行緒在不停的切換,而每次切換執行緒內的資料也在被不停的搬入搬出,會在一定程度上影響效能開銷;多核CPU的作業系統則可以並行的執行多個執行緒,理論上效能會成倍的提高。所以衍生出了多執行緒操作的概念


4、.NET中常見的執行緒物件

4 .1 多執行緒操作之Thread物件

從.NET1.0開始,我們就可以就通過Thread物件建立、控制個執行緒;簡單示例如下,我們建立了10個程式並呼叫,從結果上來看它們是多個執行緒並行執行的,且執行執行緒Id和結束的執行緒Id是可以對應上的;

4.2 多執行緒操作之ThreadPool物件

上面可以看到,每次我們需要呼叫執行緒進行操作,都需要手動建立一個執行緒物件,執行完成後再交由GC去銷燬,一定程度上會影響效能開銷,而且使用起來不是很方便,所以CLR提供了一個叫“執行緒池(ThreadPool)”的物件

執行緒池有以下特性:①當一個執行緒被使用完畢後並不會立刻被銷燬,而是放入執行緒池中等待下一次使用;當應用程式需要一個新的執行緒時,就可以從執行緒池中直接獲取一個已經存在的執行緒;②當執行緒池中的執行緒數小於執行緒池設定的下限時,執行緒池會建立新的執行緒;而當執行緒池中的執行緒數大於執行緒池設定的上限時,執行緒池將銷燬多餘的執行緒;

那麼我們怎麼操作執行緒池呢,ThreadPool物件提供了幾個靜態方法,我們使用一個簡單的,示例如下,建立一個執行緒池後,做與上一個示例相同的操作,可以看到3號執行緒被重複呼叫了兩次(隨機事件)

4.3 多執行緒操作之Task物件

上面的示例可以看到,執行緒池的使用可以複用執行緒,一定程度上可以減少系統的開銷。但是卻有幾個缺陷,比如:①不支援執行緒的掛起、取消等操作;②不支援執行緒的優先順序設定;

所以在.NET4.0又出現了Task物件,它是基於執行緒池實現的,同時彌補了執行緒池功能上的一些不足,比如可以獲取執行緒的狀態,有完全的控制權等等,我們同樣使用Task,做一個簡單的示例,可以看到,任務2可以等待任務1執行完成後再執行自己的邏輯

4.4 多執行緒操作之Parallel物件

並行Paralle內部使用的是Task物件,它提供了Parallel.Invoke, Parallel.For, Parallel.Forecah 三個方法。需要注意的是所有並行任務完成後才會返回結果,所以少量短時間任務建議不要使用Parallel。通常情況下比較適合處理密集計算的場合。我沒用過就不寫例子了?,感興趣的可以用Paralle物件的方法和for/foreach迴圈對比下看看。


5、什麼是前臺執行緒?什麼是後臺執行緒?

前面說明了一部分執行緒的概念與執行緒的操作,接下來我們來看看什麼是前臺執行緒,什麼是後臺執行緒。

預設情況下,主應用程式執行緒和Thread物件建立的執行緒會在前臺執行;而執行緒池執行緒和從非託管程式碼進入托管執行環境中的執行緒會在後臺執行,所以ThreadPool、Task和Parallel都為後臺執行緒。如果所有前臺執行緒均已終止,後臺執行緒不會保持執行, 即所有前臺執行緒停止後,CLR將停止所有後臺執行緒並關閉。示例如下:


二、執行緒的資料

1、執行緒的"私有"引數

上面提到執行緒的一個特點是可以共享資料或資源,那麼我們能定義一個引數,只供執行緒自己使用嗎?答案是肯定的,我們可以使用執行緒本地儲存(Thread Local Storage簡稱TLS)來達到這個目的。TLS是執行緒內部的一個結構,可以存放自己獨享的資料,我們可以使用Thread.GetData和Thread.SetData來獲取或設定資料。

此外.NET還封裝了一個叫ThreadStaticAttribute的物件,本質上它也是基於TLS實現的。

2、執行緒的執行上下文

什麼是執行緒執行上下文?它的英文是ExecutionContext,指執行緒執行過程中的上下文資訊。每當新建一個執行緒,該物件就會從當前建立的執行緒傳遞至被建立的執行緒,以保證被建立的執行緒與建立的執行緒有部分相同的設定資訊。

若希望手動阻止上下文的流動,可以使用ExecutionContext類中的SuppressFlow方法


三、多執行緒同步

1、什麼是多執行緒同步?

這裡的同步是指資料上的同步而非執行緒操作上的同步,當多個執行緒同一時間去訪問一個資料時,如何保證該資料的準確即是多執行緒中的重點問題之一。其實現方式都是基於鎖?實現的,簡單來說就是當一個執行緒訪問時,將資料鎖定,不允許其他執行緒訪問,下面我們介紹一下幾種型別的鎖

1.1、使用者模式構造的鎖

它使用CPU指令來協調執行緒,速度很快。它是怎麼實現同步的呢?舉個例子?:執行緒1訪問資源,使用使用者構造模式的鎖,執行緒2訪問發現有鎖後會進行等待,等待過程中會不停的去檢視資源是否可用,直到資源可用為止。

它的優點是速度快,一旦發現資源被釋放了,就立即去訪問資源;缺點就是因為它需要不停的去確認資源的狀態,所以會一直佔用CPU的資源,影響效能。綜上,它適用於對資源佔用時間短的執行緒同步場景。

.NET中提供了兩種使用者模式鎖:①Threading.Interlocked;②Thread.VolatileRead 和 Thread.VolatileWrite,它們都可以在簡單資料型別上進行讀寫操作

1.2、核心模式構造的鎖

它是對於使用者模式的一個補充。它是怎麼實現同步的呢?舉個例子?:執行緒1訪問資源,使用核心模式構造的鎖,執行緒2訪問資源發現有鎖後會被系統要求進行睡眠,執行緒1使用完資源後通知系統,系統再喚醒執行緒2。

它的優點是解決了不停去訪問資源的情況,不會佔用CPU的資源;缺點是存在使用者模式下的託管程式碼和核心程式碼相互轉換的過程,導致會延長處理時間。綜上,它適合於需要長時間佔用資源的執行緒同步場景。

.NET中提供了兩種核心模式鎖:①基於事件的,如AutoResetEvent和ManualResetEvent;②基於訊號量的,如Semaphore

1.3、混合鎖及其原理⭐️

混合鎖是基於兩者的優點實現的,執行緒使用資源的時間很短,就使用使用者模式構造同步,否則就升級到核心模式構造同步。常見的混合鎖有SemaphoreSlim、ReadWriteLockSlim和Monitor,它們有各自適用的應用場景。

下面我們看下經常使用的lock鎖,它的本質是Monitor,微軟為了開發者使用方便進行了簡單的包裝,即所謂的語法糖?,lock方法對應的主要是 Monitor的Enter和Exit方法。那麼lock是怎麼實現同步的呢?我們分三步看。

①.NET在載入時就會新建一個同步塊陣列,當物件需要被同步時,.NET會為其分配一個同步塊

②.NET在新建堆物件(即引用型別物件例項)時會分配一個名為同步索引的地址指標,初始值為-1不指向任何地址;

③使用lock時, Monitor.Enter會建立或使用一個空閒的同步索引塊,內部結構為混合鎖結構,同步索引會指向同步塊陣列為其分配的同步塊;Monitor.Exit時,會將物件的同步索引重置為-1如下圖:

再來看看經常討論的兩個問題:

①為什麼值型別不能為lock的物件?

值型別是在棧上建立的,即使裝箱後變為引用型別,因每次裝箱後地址不同,所以無法lock;

②可以lock當前物件this嗎?

this為執行程式碼的當前物件,可以被任何人訪問,會導致型別的使用者加入同步塊隊伍中,進而增加開銷;

綜上:對於例項方法的同步,一般採用私有的引用物件成員private object 名稱= new object();對於靜態方法的同步,一般採用靜態私有的引用物件成員private static object 名稱= new object();

2、互斥體Mutex和訊號量Semaphore
2.1什麼是互斥體?

它是指某些程式碼片段在任意時間內只允許一個執行緒進入。.NET中的Mutex類則是封裝好的互斥體物件。

看上去似乎和Monitor差不多,不同的是Mutex使用的是作業系統核心物件,而Monitor是在.NET下實現的,所以執行效率上Mutex會高一些;此外,Mutex可以跨應用程式域和程式,而Monitor只能同步同一應用程式域下面的執行緒。

2.2、什麼是訊號量

訊號量允許指定數量的執行緒同時訪問資源,超出數量後會進行排隊,知道之前的執行緒退出;如果Mutex是其n=1的版本,那麼訊號量就是n的版本。

訊號量適用於Web伺服器高併發的場景,它接收兩個引數,第一個為允許多少條執行緒進入(總數量),第二個為指定多少個執行緒同時進入(一次進入多少個);另外它不需要鎖的持有者,所以一般宣告為靜態型別,比如static Semaphore sem = new Semaphore(10, 2);

3、開發中的多執行緒問題
3.1、控制元件不允許跨執行緒訪問

WinForm的開發者在開發過程中使用多執行緒訪問控制元件時,經常會遇到控制元件不允許跨執行緒訪問的問題,如下圖:

那麼是什麼原因導致的呢?那是因為為了保證UI的執行緒安全,微軟在GUI應用中引入了一個特殊的執行緒處理模型,導致控制元件只能訪問由建立它的執行緒進行訪問或修改。

3.2、UI介面假死

在UI執行緒中執行耗時的計算操作,會導致UI的假死,出現該問題的原因要追溯到Windows的訊息機制。

Windows是基於訊息機制的,GUI內部就好比是一個訊息佇列,GUI執行緒不斷的迴圈處理訊息,更新UI進行呈現,如果去處理耗時操作,GUI執行緒就無法處理佇列中的其他訊息,UI介面會處於假死狀態。

如下圖,點選按鈕2時因網路原因無法獲取到對應的資訊,主執行緒被阻塞,我們是無法點選按鈕1的

3.3、如何解決

不難想到的是可以使用執行緒,但是執行緒會有更新UI的問題阿,比如上面的例子我們改成可訪問的網址又會出現不允許跨執行緒的問題,又該怎麼辦呢?

其實系統已經提供了很多處理此類問題的物件,如Invoke,BeginInvoke,BackgroundWorker等,我們這邊使用BeginInvoke進行示例,點選按鈕2後仍然可以點選按鈕1,如下圖:

關於其他物件的使用可以看看五維思考的文章 多執行緒總結(結合進度條)


四、同步非同步與多執行緒

1、什麼是同步?什麼是非同步?兩者的差異是什麼?

同步是執行或呼叫一個方法時,每次都需要拿到對應的結果才會繼續往後執行;非同步與同步相反,它會在執行或呼叫一個方法後就繼續往後執行,不會等待獲取執行結果。二者的區別就是處理請求發出後,是否需要等待請求結果,再去繼續執行其他操作。

以下圖為例,紅色線條為主執行緒,其他線條為呼叫的方法,上面的為同步,下面的為非同步。

​ (圖片來源為的劉鐵猛的視訊—C#語言入門詳解)

2、阻塞非阻塞是什麼意思?和同步非同步有關係嗎?

阻塞的概念通常會伴隨著執行緒。阻塞是指當前執行的執行緒呼叫一個方法,在該方法沒有返回值之前,當前執行的執行緒會被掛起,無法繼續進行其他操作。非阻塞是指當前執行的執行緒呼叫一個方法,當前執行的執行緒不受該方法的影響,可以繼續進行其他操作。

看完上面的說明,再對照同步非同步的說明,這不是一個意思嗎?但是它們的側重點是不同的,同步非同步強調的是是否需要等待獲取結果,而阻塞非阻塞強調的是是否會影響當前執行緒的後續操作

3、各個組合的效果

同步/非同步與阻塞/非阻塞,一共有四種組合方式,知乎上有個例子舉得很貼切,我截了張圖,原帖地址如下:

什麼是阻塞,非阻塞,同步,非同步?

4、非同步與多執行緒有關係嗎?

多執行緒是實現非同步常用的一種方式,非同步是目的,多執行緒是其實現方式之一。

下面我們通過一個使用Task執行緒實現非同步的例子,瞭解一下非同步的執行流程:

可以看到主方法的執行並沒有受到AsyncMethod方法的影響,而是繼續往下執行了,實現了非同步的效果。

5、非同步程式設計與async/await

非同步程式設計的模式在使用恰當的情況下,會帶來不小的效能提升,微軟在不同時期一共推出了三種非同步程式設計模式,分別為APM、EAP和TAP,async/await正是基於TAP模式下實現的。

5.1、async是什麼?

async 是上下文關鍵字,用來標記非同步方法,async標記方法的返回值必須是Task、Task、void之一。從C# 7.0開始,任何具有可訪問的GetAwaiter方法的型別也是可以標記的。

5.2、await是什麼?

1、 await 用於等待非同步方法的結果,await關鍵字可以用在async方法和Task、Task之前,用於等待非同步任務執行結束;

2、 await 並不是針對於async的方法,而是針對async方法所返回給我們的Task;

3、 await 不會開啟新的執行緒,直到遇到Async方法或自己建立Task,才會真正的去建立執行緒

5.3、相關說明

1、非同步方法缺少 await 不會導致編譯器錯誤,但是非同步方法會作為同步方法執行;

2、 await 無法等待具有 void 返回型別的非同步方法;

③非同步方法中無法宣告 in、ref 或 out 引數

5.4、瞭解await的基本實現

1、通過resharp可以確認,await是一個TaskAwaiter物件,而它是怎麼來的呢?原來Task類在GetAwaiter方法中建立了一個TaskAwaiter物件,並將this傳遞,如下圖:

2、接下來我們再分三個部分看:

①await如何確認後面的非同步方法執行完成了?

TaskAwaiter物件存在一個OnCompleted的方法,會等待操作完成時會執行,如下圖:

②await是怎麼讓主執行緒等待其獲取非同步方法結果的?

TaskAwaiter物件存在一個GetAwaiter的方法,會在操作完成時通知等待的物件(主執行緒),如下圖:

③await是怎麼返回結果的?

TaskAwaiter物件存在一個GetResult的方法,會結束等待並返回結果,如下圖:

結論:Task通過增加一個GetAwatier()函式,同時將自身傳遞給TaskAwaiter類來實現了await語法糖的支援


說明:多執行緒同步非同步的知識點很多,本文只是針對該部分的一個簡單小結,若想深究其原理,請檢視專業書籍。


本人知識點有限,若文中有錯誤的地方請及時指正,方便大家更好的學習和交流。

本文參考了多篇優秀的部落格內容,感興趣的朋友可以看下,地址如下:

/夢裡花落知多少/,.NET面試題解析(07)-多執行緒程式設計與執行緒同步

Edison Zhou,.NET基礎拾遺(5)多執行緒開發基礎

騰飛(Jesse),async & await 的前世今生

Jonins,非同步程式設計

相關文章