線程與同步異步
- 2020 年 5 月 2 日
- 筆記
- .NET, 【01】.NET基礎
線程與同步異步
一、線程
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
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,異步編程