C#多執行緒與非同步
1、什麼是非同步同步
如果一個方法被呼叫,呼叫者需要等待該方法被執行完畢之後才能繼續執行,則是同步。
如果方法被呼叫後立刻返回,即使該方法是一個耗時操作,也能立刻返回到呼叫者,呼叫者不需要等待該方法,則稱之為非同步。
非同步程式設計需要用到Task任務函式,不返回值的任務由 System.Threading.Tasks.Task 類表示。返回值的任務由 System.Threading.Tasks.Task<TResult> 類表示,該類從Task 繼承。
C#提供了基於任務的非同步程式設計方法(TPL),更多資料在《.NET 中的並行程式設計》https://docs.microsoft.com/zh-cn/dotnet/standard/parallel-programming/
先來個例子看下Task的基本用法。
static void Main(string[] args) { var task = new Task(() => { Console.WriteLine($"hello, task的執行緒ID為{Thread.CurrentThread.ManagedThreadId}"); Console.WriteLine("task start...."); Thread.Sleep(2000); Console.WriteLine("task end...."); }); task.Start(); Console.WriteLine("main start...."); Thread.Sleep(1000); Console.WriteLine("main end...."); Console.ReadLine(); }
執行結果為:
2、Task的三種啟動方法
通過new方法建立,然後start啟動。
通過Task.Factory.StartNew建立。
通過Task.Run建立。
Task task1 = new Task(() => Console.WriteLine($"hello, task 1的執行緒ID{Thread.CurrentThread.ManagedThreadId}");}); task1.Start(); Task task2 = Task.Run(() => {Console.WriteLine($"hello, task 2的執行緒ID{Thread.CurrentThread.ManagedThreadId}");}); Task task3 = Task.Factory.StartNew(() => {Console.WriteLine($"hello,task 3的執行緒ID為{Thread.CurrentThread.ManagedThreadId}");});
3、Task阻塞與等待
如果要等待任務完成可以使用Task.Wait方法,使用WaitAll方法等待所有任務完成。如果等待多個任務但是隻需要任何一個任務完成就可以執行下一步,則可以使用WaitAny。
static void Main(string[] args) { var t1 = Task.Run(() => { Thread.Sleep(100); Console.WriteLine($"t1執行緒ID為{ Thread.CurrentThread.ManagedThreadId}"); }); var t2 = Task.Run(() => { Thread.Sleep(100); Console.WriteLine($"t2執行緒ID為{ Thread.CurrentThread.ManagedThreadId}"); }); // t1.Wait(); var t3 = Task.Run(() => { Console.WriteLine($"t3執行緒ID為{Thread.CurrentThread.ManagedThreadId}"); Console.WriteLine("t3 start...."); Thread.Sleep(2000); Console.WriteLine("t3 end...."); });
Console.WriteLine("main start...."); Thread.Sleep(1000); Console.WriteLine("main end...."); Console.ReadLine(); }
執行效果:
如果開啟t1.Wait(); 如下t1先執行。
如果t1.Wait()改為:Task.WaitAll(t1, t2);輸出如下,先等待t1和t2執行完畢。
如果需要某個任務先執行完再執行主執行緒,則使用RunSynchronously就可以同步執行Task任務。下面的例子就是在當前的排程器上同步的執行任務t1。
var t1 = new Task(() =>
{ Thread.Sleep(100); Console.WriteLine($"t1執行緒ID為{ Thread.CurrentThread.ManagedThreadId}"); }); t1.RunSynchronously();
Console.WriteLine($"主執行緒ID為{ Thread.CurrentThread.ManagedThreadId}");
Console.ReadLine();
4、任務的狀態和返回值
Task<Tresult>獲取非同步任務的返回值。
var t1 = Task.Run(() => { Thread.Sleep(100); Console.WriteLine($"t1執行緒ID為{ Thread.CurrentThread.ManagedThreadId}"); return "t1 return"; }); Console.WriteLine($"t1 return value {t1.Result}");
Task<Tresult>會阻塞任務直到任務返回,Task.Result給出任務的返回值給呼叫它的任務,下面例子顯示了Result等待任務完成。
var cts = new CancellationTokenSource(); Task<int> t1 = new Task<int>(() => { try { //while (!cts.IsCancellationRequested) { //cts.Token.ThrowIfCancellationRequested(); Console.WriteLine($"t1 start 執行緒ID為{ Thread.CurrentThread.ManagedThreadId}"); Thread.Sleep(1000); Console.WriteLine($"t1 end 執行緒ID為{ Thread.CurrentThread.ManagedThreadId}"); } return 1; } catch (Exception ex) { Console.WriteLine("---Exception---"); return 0; } }, cts.Token); t1.Start(); Console.WriteLine("---end---{0}", t1.Result);
輸出結果如下,t.Result為1.
t.Result會導致阻塞直至等待的任務完成返回。如果使用返回值,那麼非同步方法中方法簽名返回值為Task<T>,程式碼中的返回值也要為T。
static void Main(string[] args) { var cts = new CancellationTokenSource(); Task<int> t1 = new Task<int>(() => { try { //while (!cts.IsCancellationRequested) { //cts.Token.ThrowIfCancellationRequested(); Console.WriteLine($"t1 start 執行緒ID為{ Thread.CurrentThread.ManagedThreadId}"); Thread.Sleep(1000); Console.WriteLine($"t1 end 執行緒ID為{ Thread.CurrentThread.ManagedThreadId}"); } return 1; } catch (Exception ex) { Console.WriteLine("---Exception---"); return 0; } }, cts.Token); t1.Start(); // Console.WriteLine("---end---{0}", t1.Result); var t2 = Task.Run(() => { Thread.Sleep(100); Console.WriteLine($"t2執行緒ID為{ Thread.CurrentThread.ManagedThreadId}"); }); Console.ReadLine(); } }
輸出為:
如果開啟註釋Console.WriteLine("---end---{0}", t1.Result);則任務阻塞,等待t1執行完,拿到返回值t1.Result之後才繼續執行,輸出為:
5、任務的取消
任務出錯或使用者取消了操作,則需要用到任務的取消TaskCanceledException。
cts.Cancel()設定了cts.IsCancellationRequested為true,
然後cts.Token.ThrowIfCancellationRequested()這一句丟擲了異常。
執行結果為:
如果主執行緒不延時或延時為10,則有可能在任務啟動之前就停止了任務,就不會發生任務丟擲異常了,由於系統排程問題,一定情況下輸出與上面的一樣,大部分情況輸出為:
此外,也可以在task中查詢標誌。while (!cts.IsCancellationRequested),如果取消則不在執行。如果一次取消多個任務,可以使用CancellationTokenSource.CreateLinkedTokenSource實現。
幫助文件:
https://docs.microsoft.com/zh-cn/dotnet/api/system.threading.cancellationtokensource?view=netframework-4.8
從開始.NET Framework 4,.NET Framework 使用統一的模型來協作取消非同步操作或長時間執行的同步操作涉及兩個物件:
一個CancellationTokenSource物件,它提供取消標記通過其Token屬性,並將取消訊息通過呼叫其Cancel或CancelAfter方法。
一個CancellationToken物件,指示是否請求取消。
用於實現協作取消模型的常規模式是:
例項化 CancellationTokenSource 物件,此物件管理取消通知並將其傳送給單個取消標記。
將 CancellationTokenSource.Token 屬性返回的標記傳遞給每個偵聽取消的任務或執行緒。
呼叫CancellationToken.IsCancellationRequested從接收取消標記的操作的方法。 提供有關每個任務或執行緒來響應取消請求的機制。 無論你選擇取消操作和完全操作方式,取決於你的應用程式邏輯。
呼叫 CancellationTokenSource.Cancel 方法以提供取消通知。 這將設定CancellationToken.IsCancellationRequested到取消標記的每個副本上的屬性true。
呼叫Dispose方法在您使用完CancellationTokenSource物件。
6、Task和Thread
Task是基於Thread的,是比較高層級的封裝,Task任務最終還是需要Thread來執行。
Task比Thread的開銷小,Task預設使用執行緒池。
Task預設使用後臺執行緒來執行,Thread預設是前臺執行緒。
Task使用引數和返回值都比Thread簡單。
Task的多工排程比Thread靈活,Thead的join需要掛起呼叫者去執行被呼叫執行緒然後返回到主執行緒。而Task提供了多種Wait函式,可以等待其他執行緒執行。
7、參考文件
本文只是學習筆記,水平有限,拋磚迎玉。
&nbs1、基於任務的非同步程式設計
https://docs.microsoft.com/zh-cn/dotnet/standard/parallel-programming/task-based-asynchronous-programming
2、Task 類
https://docs.microsoft.com/zh-cn/dotnet/api/system.threading.tasks.task?view=netframework-4.8
3、Difference Between Task and Thread
https://www.dotnetforall.com/difference-task-and-thread/
4、Task And Thread In C#
https://www.c-sharpcorner.com/article/task-and-thread-in-c-sharp/
5、C#多執行緒和非同步(一)——基本概念和使用方法
https://www.cnblogs.com/wyy1234/p/9166444.html