多執行緒技術的歷史發展與簡單使用
學習自:
C# BackgroundWorker 詳解_20160925
非同步編程系列(Thread、Task、async/await、ajax等)_20130426
進程與執行緒
進程是應用的執行實例,可狹義理解為一個應用程式就是一個進程。啟用一個應用程式時就是啟動了一個進程。該應用運行所需的所有地址空間,程式碼,數據及系統資源都屬於此進程。進程所使用的所有資源會在進程終止時被釋放或關閉。
執行緒是進程內部的一個執行單元。啟動進程的同時就會啟動該進程的主執行緒。一個進程可以包含很多執行緒。
執行緒分類
執行緒有很多種分類
從系統回收的角度來說
可分為前台執行緒和後台執行緒
-
前台執行緒
前台執行緒不會受外在原因影響,只會在自己執行完成時關閉。假設一個應用程式啟動了一個前台執行緒寫文件,隨後關閉應用程式,應用程式的前台執行緒終止,但CLR依舊保持活動並運行,使應用程式還會繼續運行,只有寫文件的這個前台執行緒完成,終止後,整個進程才會被銷毀,執行緒才被回收。
-
後台執行緒
後台執行緒可以隨時被CLR關閉且不會引發異常。也就是說後台執行緒被關閉時,資源的回收是立即的,不會等待的,不會考慮後台執行緒是否執行完畢。即使正在執行中也會被立即終止。
從創建方式來說
初期使用Thread
類new
創建的執行緒都是專用執行緒(默認為前台執行緒),從推出程池ThreadPool
後,基於ThreadPool
的執行緒都稱為執行緒池執行緒(默認為後台執行緒)。
從執行緒池執行緒的功能來說
可分為工作執行緒與I/O執行緒
- 工作執行緒:執行普通操作
- I/O執行緒:專用於非同步I/O操作,如文件讀寫,網路請求
注意:
- 進程(應用程式)會等待所有的前台執行緒完成後再結束本工作;但是如果只剩下後台執行緒,則會直接結束本工作,不會等待後台執行緒完成後再結束本工作。
- 在任何時候我們都可以通過執行緒的
IsBackground
屬性改變執行緒的前後台屬性 - 應用程式的主執行緒以及使用Thread構造的執行緒都默認為前台執行緒
- 基於執行緒池
ThreadPool
功能創建的執行緒都默認為後台執行緒 - 不涉及一些專用的,長時間保持運行的功能,都建議使用後台執行緒。
- I/O 非同步執行緒數,這個執行緒數限制的是執行非同步委託的執行緒數量
非同步編程
底層技術發展歷程簡述
Thread < ThreadPool < Task < Async/Await
性能越來越好
非同步編程模式發展簡述
- 應用Thread
- 應用TheadPool
- APM(Asynchronous Programming Model):非同步編程模型
- EAP(Event-based Asynchornous Pattern):基於事件的非同步編程模式
- TAP(Task-based Asynchronous Pattern):基於任務的非同步編程模式
- 應用Async/Await
ThreadPool
解決問題:
解決頻繁創建,銷毀執行緒十分耗時的問題。
創建和銷毀執行緒是十分消耗CPU資源的操作,也就是十分耗時的操作。頻繁創建、銷毀執行緒會影響應用程式性能。所以引入快取來解決這個問題。創建一些執行緒後不銷毀,而是保存在一些地方,需要使用執行緒時,調用這些已有執行緒就可以。節省了創建、銷毀執行緒的時間。
APM(Asynchronous Programming Model)
非同步編程模型
基於IAsyncResult介面實現
解決的問題:
解決ThreadPool中沒有反應非同步操作狀態的機制,無法獲取非同步操作返回值的問題。
特徵:
- 實現IAsyncResult介面
- Beginxxx方法,啟動非同步操作
- Endxxx方法,結束非同步操作
非同步方法相比於同步方法多2個參數(不過也都可以為null)
-
AsyncCallback callback
AsyncCallback
是一個委託delegate void AsyncCallback(IAsyncResult ar)
,此委託綁定非同步操作完成時要調用的方法 -
Object object
一個用戶可以自定義的對象,此對象可用來向非同步操作完成時為
AsyncCallback
委託方法傳遞應用程式特定的狀態資訊,也可通過此對象在委託中訪問Endxxx
方法。
注意:
-
其中
Beginxxx
方法返回IAsyncResult
,Endxxx
方法返回與同步方法相同的返回值。 -
Beginxxx
方法啟動非同步操作在另一個執行緒執行時,若想要獲取其非同步操作的返回值,需調用Endxxx
方法來獲取。 -
那如果我們的非同步操作不需要返回值就可以在
Beginxxx
方法啟動非同步操作後,不調用Endxxx
方法來終止非同步操作嗎?答案是不行。
Beginxxx
方法後必須調用Endxxx
方法來終止。原因有2。第一,
Beginxxx
方法啟動非同步操作後,會被分配一些資源,這些資料會一直保持到調用Endxxx
方法才會釋放。第二,即使我們的非同步操作沒有返回值,我們也需要知道我們的非同步操作是否執行完畢,是否出錯,出了什麼錯等等資訊,這些資訊都需要我們通過調用
Endxxx
方法老獲取。 -
APM中,我們想要在非同步完成時執行一些操作怎麼辦?
可以通過在
Beginxxx
方法的AsyncCallback callback
參數中傳遞迴調方法來做非同步後的其他處理。
使用委託進行非同步編程
C#中的委託自動為我們提供了同步調用方法Invoke
與非同步調用方法BeginInvoke
與EndInvoke
。
非同步委託是快速構建非同步調用的方式之一,它就是基於IAsyncResult
實現的,通過BeginInvoke
返回IAsyncResult
對象,通過EndInvoke
獲取結果。
最大的缺陷:
沒有提供進度通知等功能及多執行緒間控制項的訪問
特別聲明
.NET Core
以後不再支援非同步委託(可狹義理解為不再支援APM那種形式),只能在.NET Framework
中使用。在.NET Core
中使用後會報錯:System.PlatformNotSupportedException:「Operation is not supported on this platform.」
原因:
Async delegates are not in .NET Core for several reasons:
*Async delegates use deprecated IAsyncResult-based async pattern. This pattern is generally not supported throughout .NET Core base libraries, e.g. System.IO.Stream does not have IAsyncResult-based overloads for Read/Write methods in .NET Core.
Async delegates depend on remoting (System.Runtime.Remoting) under the hood. Remoting is not in .NET Core – implementation of async delegates without remoting would be challenging.
非同步委託不再應用於.NET Core
的原因:
非同步委託使用已棄用的基於IAsyncResult
的非同步模式(也就是APM),這種模式不再受.NET Core
基礎庫的支援。例如,在.NET Core
中System.IO.Stream
已經沒有了基於IAsyncResult
的重載方法。
非同步委託依賴於Remoting (System.Runtime.Remoting)
。.NET Core
中已經沒有了Remoting
。在沒有Remoting
的情況下實現非同步委託是一個挑戰。
個人補充:反正就是不支援了,這種舊程式碼能看懂就基本可以了。我們使用的話肯定是用新不用舊。另.NET的官方文檔其實是有非同步委託的相關示例的,這裡猜測可能是服務於APM轉TAP的情況吧。
EAP(Event-based Asynchronous Pattern)
基於事件的非同步編程模式
關鍵的基礎設施:
- 事件
- AsyncOperation類
- AsyncOperationManager類
基於事件的非同步編程模式的主要功能:
- 非同步執行耗時的操作
- 獲取進度報告和增量結果
- 支援非同步耗時任務的取消
- 可以獲取非同步耗時任務的結果數據或異常資訊
- 支援同時執行多個非同步操作,及獲取他們的進度報告,增量結果,取消操作,返回結果或異常資訊
- 對於簡單的多執行緒應用,提供BackgroundWorker組件可以快速搭建簡單的解決方案。
優點:
- 與Visual Studio UI設計器有很好的集成
- 通過內部的SynchronizationContext類,可以很方便的跨執行緒操作控制項。
特徵:
- 簡單情況:一個xxxAsync方法對應一個xxxCompleted事件,以及其同步版本
- 複雜情況:多個xxxAsync方法對應其各自的xxxCompleted事件,及其同步版本
- 更複雜的情況:非同步方法支援取消(CancelAsync()方法),支援進度報告(ReportProgress() 方法),支援增量結果(ProgressChanged事件)
- 如果不想支援多個並發調用,可考慮公開IsBusy屬性
- 如要非同步操作的同步版本中有 Out 和 Ref 參數,它們應做為對應 xxxCompletedEventArgs的一部分
BackgroundWorker組件
它是System.ComponentModel命名空間為我們提供的一個簡單的多執行緒應用解決方案,它允許在單獨的執行緒上運行耗時操作而不會導致用戶介面阻塞。但是注意,它同一時刻只能運行一個非同步耗時操作(使用IsBusy屬性判定),並且不能誇AppDomain邊界進行封送處理(也就是不能在多個AppDomain中執行多執行緒操作)
BackgroundWorker bgWorker;
private void Bt15_Click(object sender, RoutedEventArgs e)
{
bgWorker = new BackgroundWorker();
// 註冊非同步執行事件
bgWorker.DoWork += BgWorker_DoWork;
// 註冊完成事件
bgWorker.RunWorkerCompleted += BgWorker_RunWorkerCompleted;
// 使能獲取進度的功能
bgWorker.WorkerReportsProgress = true;
// 註冊獲取進度事件
bgWorker.ProgressChanged += BgWorker_ProgressChanged;
// 啟動非同步執行
bgWorker.RunWorkerAsync(5);
}
private void BgWorker_DoWork(object sender, DoWorkEventArgs e){/* Dosomething */}
private void BgWorker_ProgressChanged(object sender, ProgressChangedEventArgs e){/* Dosomething */}
private void BgWorker_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e){/* Dosomething */}
private void Bt16_Click(object sender, RoutedEventArgs e)
{
// 使能終止任務的功能
bgWorker.WorkerSupportsCancellation = true;
// 終止任務
bgWorker.CancelAsync();
}
注意:
-
如何向
DoWork
中傳遞參數?bgWorker.RunWorkerAsync(object);
可傳遞object
變數,在DoWork
中用e.Argument
獲取。 -
如何獲取進度資訊?
使能標識
bgWorker.WorkerReportsProgress = true
,註冊事件
ProgressChanged
然後通過
bgWorker.ReportProgress(i, message);
函數啟動第一個參數類型為
int
,表示執行進度。第二個參數為
object
,可以傳遞我們的自定義資訊,在ProgressChanged
中通過e.UserState
獲取 -
Dowork
中怎麼向RunWorkerCompleted
傳遞參數?在
DoWork
中設置e.Result
,在RunWorkerCompleted
的e.Result
中就可以獲取到。 -
如何停止任務?
使能標識
bgWorker.WorkerSupportsCancellation = true;
通過函數
bgWorker.CancelAsync();
停止注意:我們要自己在非同步程式碼中編寫捕獲停止標識的程式碼,來控制非同步程式碼的停止與退出。不是執行了
CancelAsync();
任務就會停止退出。
TAP(Task-based Asynchronous Pattern)
TPL(Task Parallel library)
TAP:基於任務的非同步編程模型
TPL:任務並行庫
好多文章都會把這兩個混著說,實在是讓人非常迷茫。查詢了很多說法,其中我比較能認同。來自於【轉】Difference between TPL and TAP_20201201
I want to clarify the difference between two these abbreviations: TPL(a task parallel library) and TAP (task async pattern).
AFAIU, TPL – is a task parallel library and the main part of this library is Task and all related staff. So, it’s like a technology which was implemented by Microsoft.
TAP – it’s a pattern which underlies to async/await syntax sugar. And which is based on callback function + state machine + SynchronizationContext logic.
Is there something to add or correct?
A:
TPL is a part of the BCL. It includes Task as well as several other parallelism-related higher-level abstractions including Parallel and Parallel LINQ. The focus of TPL was parallel processing, and using tasks as futures – while supported – was a relatively unused feature.
TAP is a pattern. It’s called “Task-based” because it reused the Task type from the TPL as a generic Future type. Task (and related types) were enhanced to include more primitives to support TAP and asynchronous programming (e.g., GetAwaiter(), Task.WhenAll, etc). These days, TAP also works with “tasklikes” including ValueTask. TAP is focused on asynchronous programming as opposed to parallel processing.
我想說清這兩個縮寫之間的區別:TPL(Task Parallel library)和TAP(Task-based Asynchronous Pattern)。
據我所知(AFAIU:as far as I understand),TPL-是一個任務並行庫,主要包含Task與所有其相關構成,它更偏向於是微軟架設的一種底層技術。
TAP-是async/await語法糖的基礎模式。是一種基於回調函數,狀態機,與同步上下文邏輯(SynchronizationContext)的一種模式。
TPL是BCL的一部分。它包括Task以及其他許多包括Parallel and Parallel LINQ在內的與並行性相關的高級抽象。TPL專註於解決並行處理。在TPL中使用了tasks作為futures,是一直受支援的,但相對來說tasks是不怎麼被使用的功能。
TAP是一種模式,它被成為「基於Task的」,因為它復用重用了TPL中的Task作為一個通用的Funture類型。Task(和其相關類型)都被增強了,以包含更過支援TAP和非同步編程的原語(如,GetAwaiter()、Task.WhenAll 等)。如今,TAP還與包括ValueTask的「類tasks」型一起工作。TAP專註於處理非同步編程問題,而不是並行處理。
個人理解:
TAP是基於TPL的。TPL其實與非同步編程不是一個賽道的。最好不要混著說。
Future type
:感覺像是各種語言默認的與並行還是非同步相關的一個類型,具體翻譯成啥也說不好。
async/await
async/await關鍵字,主要用於我們使用順序結構(而不是使用回調)來實現非同步編程。極大增強非同步編程的可讀性。
下述非同步方法即為:async
或await
關鍵字修飾的方法
注意:
-
非同步方法的參數:不能使用「ref」參數和「out」參數,但是在非同步方法內部可以調用含有這些參數的方法
-
非同步方法的返回類型:返回類型有且只有3種,
Task
,Task<TResult>
,void
。其中Task
代表非同步方法沒有返回值Task<TResult>
代表非同步方法有返回值,且返回值類型為TResultvoid
主要用於事件處理程式(不能被等待,無法捕獲異常),也可以說只是為了兼容一些舊版本程式碼。 -
async
和await
關鍵字不會導致其他執行緒的創建,只有當await
等待任務運行時,非同步方法才會將控制權轉移給非同步方法外部,讓其不受阻塞的執行。待await
等待的任務執行完畢再將控制權轉移給await
處,繼續執行非同步方法後續的程式碼。補充上一句,上一句的「只有當
await
等待任務運行時,非同步方法才會將控制權轉移給非同步方法外部」會讓人感覺是await
關鍵字創建了新執行緒,但其實不是。await修飾的方法中依舊會存在「同步」的程式碼,真正創建新執行緒的方法還是方法中的Task.Run()
或其他Task
相關的程式碼。但那句話也不是不對,因為await
修飾的程式碼必須返回Task
或Task<TResult>
,否則就會報錯無法執行。 -
被「async」關鍵字標記的方法不會被轉換為非同步方式。
Task
Task.Run(~)
Para:(Action action),Return:Task
Queues the specified work to run on the thread pool and returns a System.Threading.Tasks.Task object that represents that work.
將指定工作排入執行緒池的工作隊列,並返回一個Task代表這個工作。
Task.ContinueWith(~)
Creates a continuation that executes asynchronously when the target System.Threading.Tasks.Task completes.
創建一個伴隨程式,當非同步Task執行完畢的時候執行。
Para:(Action continuationAction),Return:Task
An action to run when the System.Threading.Tasks.Task completes. When run, the delegate will be passed the completed task as an argument.
只有一個參數 continuationAction時,它代表Task完成時所要運行的操作。該操作運行時,將會把已完成的任務作為參數傳入委託。
Task.WaitAll(~)
Waits for all of the provided System.Threading.Tasks.Task objects to complete execution.
等待所有提供的Task執行完成。
就只單純的等,相當於到這就停住,該方法包含的所有Task執行完畢後,才可以執行後續處理。
Q&A
什麼是執行緒上下文
當系統從一個執行緒切換到另一個執行緒時,它將保存被搶先的執行緒的執行緒上下文,並重新載入執行緒隊列中下一個執行緒的已保存的執行緒上下文。
個人理解就是執行緒需要保存的數據和資源。一般英文文檔中的xxxContext
都會被翻譯為「xxx上下文」,個人認為是挺隔路。隔路的點在於,英文文檔中的xxxContext
都是表示該對象的內容,但漢語語境中,「xxx上下文」,通常會理解為除該對象以外的內容。
前台執行緒與後台執行緒的區別
這個根據要表達的重點不同會有很多表述。其核心功能可狹義理解為前台執行緒不受外在因素影響,啟動後必須執行完才停止。而後台執行緒受其他因素控制,執行過程中也可立即停止。
一個顯著的例子就是若應用程式啟動了一個前台執行緒,退出應用程式後,前台執行緒還會繼續執行(也就是應用程式其實並沒有真正「退出」,資源也沒有釋放)。若應用程式啟動的是後台執行緒,退出應用程式後,後台執行緒也會停止執行並釋放。
所以使用前台執行緒時要注意避免遺留為停止的前台執行緒,會導致應用程式無法停止。
低優先順序的執行緒會等待高優先順序的執行緒執行完再執行嗎?
不會,低優先順序的執行緒不會被阻塞。低優先順序的執行緒相比於高優先順序的執行緒,只是在相同時間間隔內,被CPU調度的次數相對少而已。
執行緒池出現的原因
創建和銷毀執行緒是十分消耗CPU資源的操作,也就是十分耗時的操作。頻繁創建、銷毀執行緒會影響應用程式性能。所以引入快取來解決這個問題。創建一些執行緒後不銷毀,而是保存在一些地方,需要使用執行緒時,調用這些已有執行緒就可以。節省了創建、銷毀執行緒的時間。
系統,程式中的池是什麼
我們編程過程中或多或少都接觸過各種「池」,比如,資料庫連接池,執行緒池,socket連接池等等。這些池的主要用途都是一個:把系統需要頻繁使用的對象保存起來,供系統調用,節省對象重複創建與銷毀多耗費的時間。是一種「空間換時間」的處理機制。
當然把對象保存起來並不能解決問題,我們還需要解決快取的大小問題、排隊執行任務、調度空閑執行緒、按需創建新執行緒及銷毀多餘空閑執行緒……等等問題。而微軟的團隊已經都為我們解決好了這些問題,也就是ThreadPool
類,我們只需要調用類中的方法就可以了。這樣我就就可以專註於程式業務功能而不是執行緒管理。
並行與並發的區別
並行:多個處理核心同一時刻同時處理多個不同的任務。
並發:一個處理核心在同一時間段處理多個不同任務,各個任務快速交替執行。即同一時刻,其實只有一個任務在執行。
什麼是任務的全局隊列與局部隊列
在主執行緒或其他並沒有分配給某個特定任務的執行緒的上下文中創建並啟動的任務,這些任務將會在全局隊列中競爭工作執行緒。這些任務被稱為頂層任務。
如果是在其他任務的上下文中創建的任務(子任務或嵌套任務),這些任務將被分配在執行緒的局部隊列中。
全局隊列的調用順序是FIFO
局部隊列的調用順序通常是LIFO
為什麼會出現任務的局部隊列這種機制
執行緒的全局隊列是共享資源,所以內部會實現一個鎖機制。當一個任務內部會創建很多子任務時,並且這些子任務完成得非常快,就會造成頻繁的進入全局隊列和移出全局隊列,從而降低應用程式的性能。
為了避免這種情況,執行緒池引擎為每個執行緒引入了局部隊列。
局部隊列有2個性能優勢:任務內聯化和工作竊取
什麼是任務內聯化
僅當執行緒等待時出現
是執行緒的局部隊列帶來的性能優化方法。是利用阻塞的頂層任務的執行緒去執行局部隊列中的任務,減少了額外執行緒的開銷。
如一個頂層任務需要等待3個嵌套任務執行完畢再執行,其中一個嵌套任務就可以運行在正在等待的頂層任務的執行緒中,這樣就減少了一個額外執行緒的開銷。
什麼是工作竊取
就是讓空閑的工作執行緒,來進入局部隊列執行局部隊列中正在等待的任務。
async會創建新執行緒還是await會創建新執行緒
都不會,async/await可以理解為一種非同步的結構同步化語法糖,具體的新執行緒還是通過Task.Run()
等程式碼創建。
在await的程式碼中不返回Task,返回void不行嗎
不行,await後面跟著的必須是一個等待表達式,如Task
,Task<TResult>
。返回void,或其他參數會報錯。”CS4008:無法等待void”或「CS1061:bool未包含GetAwaiter的定義,並且找不到可接受第一個bool類型參數的可訪問擴展方法GetAwaiter(是否缺少 using 指令或程式集引用?)」
以前的非同步編程怎麼實現順序執行
在非同步程式碼內連續委託,回調。
非同步編程模式的逐步發展主要為了什麼
除去基礎設施的完善。非同步編程的發展主要為了編碼人員能夠更加簡單的編寫出非同步程式。由最初的Thread
發展至目前常用的async\await
關鍵字。逐步解決了執行緒頻繁創建的問題,執行緒管理的問題,APM或EAP模式需要手寫大量程式碼,又因為委託、回調導致程式碼可讀性很差,控制流混亂的問題。最終可以讓我們以一種類似於同步的結構來編寫非同步程式碼,極大的減少了編寫難度,增強了可讀性。
非同步編程本質是為了什麼
這個一定是有很多的用處,但目前就我個人來說,最大的用處就是
使用非同步處理一些耗時操作,保證UI執行緒的執行緒能力,提高用戶體驗。
Thread.sleep()究竟是讓那個執行緒停止。
解析一個場景
假設一個需求:我們需要從資料庫中查詢一個數據,並將查詢結果顯示到頁面中。
假設查詢資料庫的方法為GetResult()
,其至少需要5s。介面上顯示的控制項為TBResult
我們會有幾種解決方式。
1,同步方式
string result = GetResult();
TBResult.Text = result;
最大的弊病:
查詢資料庫的這至少5秒時間,整個應用是阻塞的,也就是不能操作的,簡單來說就是卡死的。
2.Thread非同步
Thread t = new Thread(() =>
{
string result = GetResult();
this.Dispatcher.Invoke(() =>
{
TBResult.Text = str;;
});
});
t.Start();
一種方法是直接在執行緒中操作
Thread t = new Thread(() =>
{
Action<string> callback = new Action<string>(ThreadCallBackAction);
string result = GetResult();
callback(result);
});
t.Start();
private void ThreadCallBackAction(string str)
{
this.Dispatcher.Invoke(() =>
{
TBResult.Text = str;;
});
}
另一種方法是利用委託來實現
弊病:
- 我們無法在外部正常獲取Thread的返回值,也無法知道Thread什麼時候執行完畢,已經獲取到了值。
- Thread創建的執行緒是前台執行緒,很可能會造成執行緒問題,我們需要自己進行管理。
3.ThreadPool非同步
ThreadPool.QueueUserWorkItem(new WaitCallback((s) =>
{
Action<string> callback = new Action<string>(ThreadPoolCallBackAction);
string result = GetResult();
callback(result);
});
private void ThreadPoolCallBackAction(string str)
{
this.Dispatcher.Invoke(() =>
{
TBResult.Text = str;;
});
}
其實操作方式與Thread幾乎一樣,但是使用執行緒池我們就不用自己管理執行緒了。CLR引擎會替我們解決執行緒管理的問題
4.Task非同步
var ResultTask = Task.Run(() => {
string result = GetResult();
return result;
});
ResultTask.ContinueWith((ResultTask)=>
{
this.Dispatcher.Invoke(() =>
{
TxtRes.Text = ResultTask.Result;
});
});
使用Task,我們終於擺脫了複雜的回調,使用Task的ContinueWith方法就可以在指定任務執行結束後在執行其他任務。
5.Asyn/Await非同步
private async void Button_Click(object sender, RoutedEventArgs e)
{
string res = await Task.Run(() =>
{
string result = GetResult();
return result;
});
TxtRes.Text = res;
}
省去了複雜的Task方法調用過程,也省去了UI執行緒的委託過程。雖然是非同步,但程式碼像同步一樣邏輯簡潔。