C#多線程系列之任務(wù)基礎(chǔ)(一)
多線程編程
多線程編程模式
.NET 中,有三種異步編程模式,分別是基于任務(wù)的異步模式(TAP)、基于事件的異步模式(EAP)、異步編程模式(APM)。
- 基于任務(wù)的異步模式 (TAP) :.NET 推薦使用的異步編程方法,該模式使用單一方法表示異步操作的開(kāi)始和完成。包括我們常用的 async 、await 關(guān)鍵字,屬于該模式的支持。
- 基于事件的異步模式 (EAP) :是提供異步行為的基于事件的舊模型。《C#多線程(12):線程池》中提到過(guò)此模式,.NET Core 已經(jīng)不支持。
- 異步編程模型 (APM) 模式:也稱為 IAsyncResult 模式,,這是使用 IAsyncResult 接口提供異步行為的舊模型。.NET Core 也不支持,請(qǐng)參考 《C#多線程(12):線程池》。
前面,我們學(xué)習(xí)了三部分的內(nèi)容:
- 線程基礎(chǔ):如何創(chuàng)建線程、獲取線程信息以及等待線程完成任務(wù);
- 線程同步:探究各種方式實(shí)現(xiàn)進(jìn)程和線程同步,以及線程等待;
- 線程池:線程池的優(yōu)點(diǎn)和使用方法,基于任務(wù)的操作;
這篇開(kāi)始探究任務(wù)和異步,而任務(wù)和異步是十分復(fù)雜的,內(nèi)容錯(cuò)綜復(fù)雜,筆者可能講不好。。。
探究?jī)?yōu)點(diǎn)
我們現(xiàn)在來(lái)探究一下多線程編程的復(fù)雜性。
- 傳遞數(shù)據(jù)和返回結(jié)果
傳遞數(shù)據(jù)倒是沒(méi)啥問(wèn)題,只是難以獲取到線程的返回值,處理線程的異常也需要技巧。
- 監(jiān)控線程的狀態(tài)
新建新的線程后,如果需要確定新線程在何時(shí)完成,需要自旋或阻塞等方式等待。
- 線程安全
設(shè)計(jì)時(shí)要考慮如果避免死鎖、合理使用各種同步鎖,要考慮原子操作,同步信號(hào)的處理需要技巧。
- 性能
玩多線程,最大需求就是提升性能,但是多線程中有很多坑,使用不當(dāng)反而影響性能。
[以上總結(jié)可參考《C# 7.0本質(zhì)論》19.3節(jié),《C# 7.0核心技術(shù)指南》14.3 節(jié)]
我們通過(guò)使用線程池,可以解決上面的部分問(wèn)題,但是還有更加好的選擇,就是 Task(任務(wù))。另外 Task 也是異步編程的基礎(chǔ)類型,后面很多內(nèi)容要圍繞 Task 展開(kāi)。
原理的東西,還是多參考微軟官方文檔和書籍,筆者講得不一定準(zhǔn)確,而且不會(huì)深入說(shuō)明這些。
任務(wù)操作
任務(wù)(Task)實(shí)在太多 API 了,也有各種騷操作,要講清楚實(shí)在不容易,我們要慢慢來(lái),一點(diǎn)點(diǎn)進(jìn)步,一點(diǎn)點(diǎn)深入,多寫代碼測(cè)試。
下面與筆者一起,一步步熟悉、摸索 Task 的 API。
兩種創(chuàng)建任務(wù)的方式
通過(guò)其構(gòu)造函數(shù)創(chuàng)建一個(gè)任務(wù),其構(gòu)造函數(shù)定義為:
public Task (Action action);
其示例如下:
class Program { static void Main() { // 定義兩個(gè)任務(wù) Task task1 = new Task(()=> { Console.WriteLine("① 開(kāi)始執(zhí)行"); Thread.Sleep(TimeSpan.FromSeconds(1)); Console.WriteLine("① 執(zhí)行中"); Thread.Sleep(TimeSpan.FromSeconds(1)); Console.WriteLine("① 執(zhí)行即將結(jié)束"); }); Task task2 = new Task(MyTask); // 開(kāi)始任務(wù) task1.Start(); task2.Start(); Console.ReadKey(); } private static void MyTask() { Console.WriteLine("② 開(kāi)始執(zhí)行"); Thread.Sleep(TimeSpan.FromSeconds(1)); Console.WriteLine("② 執(zhí)行中"); Thread.Sleep(TimeSpan.FromSeconds(1)); Console.WriteLine("② 執(zhí)行即將結(jié)束"); } }
.Start()
方法用于啟動(dòng)一個(gè)任務(wù)。微軟文檔解釋:?jiǎn)?dòng) Task,并將它安排到當(dāng)前的 TaskScheduler 中執(zhí)行。
TaskScheduler 這個(gè)東西,我們后面講,別急。
另一種方式則使用 Task.Factory
,此屬性用于創(chuàng)建和配置 Task
和 Task<TResult>
實(shí)例的工廠方法。
使用https://docs.microsoft.com/zh-cn/dotnet/api/system.threading.tasks.taskfactory.startnew?view=netcore-3.1#--可以添加任務(wù)。
當(dāng)需要對(duì)長(zhǎng)時(shí)間運(yùn)行、計(jì)算限制的任務(wù)(計(jì)算密集型)進(jìn)行精細(xì)控制時(shí)才使用 StartNew() 方法;
官方推薦使用 Task.Run 方法啟動(dòng)計(jì)算限制任務(wù)。
Task.Factory.StartNew() 可以實(shí)現(xiàn)比 Task.Run() 更細(xì)粒度的控制。
Task.Factory.StartNew()
的重載方法是真的多,你可以參考: https://docs.microsoft.com/zh-cn/dotnet/api/system.threading.tasks.taskfactory.startnew?view=netcore-3.1#--
這里我們使用兩個(gè)重載方法編寫示例:
public Task StartNew(Action action);
public Task StartNew(Action action, TaskCreationOptions creationOptions);
代碼示例如下:
class Program { static void Main() { // 重載方法 1 Task.Factory.StartNew(() => { Console.WriteLine("① 開(kāi)始執(zhí)行"); Thread.Sleep(TimeSpan.FromSeconds(1)); Console.WriteLine("① 執(zhí)行中"); Thread.Sleep(TimeSpan.FromSeconds(1)); Console.WriteLine("① 執(zhí)行即將結(jié)束"); }); // 重載方法 1 Task.Factory.StartNew(MyTask); // 重載方法 2 Task.Factory.StartNew(() => { Console.WriteLine("① 開(kāi)始執(zhí)行"); Thread.Sleep(TimeSpan.FromSeconds(1)); Console.WriteLine("① 執(zhí)行中"); Thread.Sleep(TimeSpan.FromSeconds(1)); Console.WriteLine("① 執(zhí)行即將結(jié)束"); },TaskCreationOptions.LongRunning); Console.ReadKey(); } // public delegate void TimerCallback(object? state); private static void MyTask() { Console.WriteLine("② 開(kāi)始執(zhí)行"); Thread.Sleep(TimeSpan.FromSeconds(1)); Console.WriteLine("② 執(zhí)行中"); Thread.Sleep(TimeSpan.FromSeconds(1)); Console.WriteLine("② 執(zhí)行即將結(jié)束"); } }
通過(guò) Task.Factory.StartNew()
方法添加的任務(wù),會(huì)進(jìn)入線程池任務(wù)隊(duì)列然后自動(dòng)執(zhí)行,不需要手動(dòng)啟動(dòng)。
TaskCreationOptions.LongRunning
是控制任務(wù)創(chuàng)建特性的枚舉,后面講。
Task.Run() 創(chuàng)建任務(wù)
Task.Run()
創(chuàng)建任務(wù),跟 Task.Factory.StartNew()
差不多,當(dāng)然 Task.Run()
還有很多重載方法和騷操作,我們后面再來(lái)學(xué)。
Task.Run()
創(chuàng)建任務(wù)示例代碼如下:
static void Main() { Task.Run(() => { Console.WriteLine("① 開(kāi)始執(zhí)行"); Thread.Sleep(TimeSpan.FromSeconds(1)); Console.WriteLine("① 執(zhí)行中"); Thread.Sleep(TimeSpan.FromSeconds(1)); Console.WriteLine("① 執(zhí)行即將結(jié)束"); }); Console.ReadKey(); }
取消任務(wù)
取消任務(wù),《C#多線程(12):線程池》 中說(shuō)過(guò)一次,不過(guò)控制太自由,全靠任務(wù)本身自覺(jué)判斷是否取消。
這里我們通過(guò) Task 來(lái)實(shí)現(xiàn)任務(wù)的取消,其取消是實(shí)時(shí)的、自動(dòng)的,并且不需要手工控制。
其構(gòu)造函數(shù)如下:
public Task StartNew(Action action, CancellationToken cancellationToken);
代碼示例如下:
按下回車鍵的時(shí)候記得切換字母模式。
class Program { static void Main() { Console.WriteLine("任務(wù)開(kāi)始啟動(dòng),按下任意鍵,取消執(zhí)行任務(wù)"); CancellationTokenSource cts = new CancellationTokenSource(); Task.Factory.StartNew(MyTask, cts.Token); Console.ReadKey(); cts.Cancel(); // 取消任務(wù) Console.ReadKey(); } // public delegate void TimerCallback(object? state); private static void MyTask() { Console.WriteLine(" 開(kāi)始執(zhí)行"); int i = 0; while (true) { Console.WriteLine($" 第{i}次任務(wù)"); Thread.Sleep(TimeSpan.FromSeconds(1)); Console.WriteLine(" 執(zhí)行中"); Thread.Sleep(TimeSpan.FromSeconds(1)); Console.WriteLine(" 執(zhí)行結(jié)束"); i++; } } }
父子任務(wù)
前面創(chuàng)建任務(wù)的時(shí)候,我們碰到了 TaskCreationOptions.LongRunning
這個(gè)枚舉類型,這個(gè)枚舉用于控制任務(wù)的創(chuàng)建以及設(shè)定任務(wù)的行為。
其枚舉如下:
枚舉 | 值 | 說(shuō)明 |
---|---|---|
AttachedToParent | 4 | 指定將任務(wù)附加到任務(wù)層次結(jié)構(gòu)中的某個(gè)父級(jí)。 |
DenyChildAttach | 8 | 指定任何嘗試作為附加的子任務(wù)執(zhí)行的子任務(wù)都無(wú)法附加到父任務(wù),會(huì)改成作為分離的子任務(wù)執(zhí)行。 |
HideScheduler | 16 | 防止環(huán)境計(jì)劃程序被視為已創(chuàng)建任務(wù)的當(dāng)前計(jì)劃程序。 |
LongRunning | 2 | 指定任務(wù)將是長(zhǎng)時(shí)間運(yùn)行的、粗粒度的操作,涉及比細(xì)化的系統(tǒng)更少、更大的組件。 |
None | 0 | 指定應(yīng)使用默認(rèn)行為。 |
PreferFairness | 1 | 提示 TaskScheduler 以一種盡可能公平的方式安排任務(wù)。 |
RunContinuationsAsynchronously | 64 | 強(qiáng)制異步執(zhí)行添加到當(dāng)前任務(wù)的延續(xù)任務(wù)。 |
這個(gè)枚舉在 TaskFactory
和 TaskFactory<TResult>
、Task
和 Task<TResult>
、
StartNew()
、FromAsync()
、TaskCompletionSource<TResult>
等地方可以使用到。
子任務(wù)使用了 TaskCreationOptions.AttachedToParent ,并不是指父任務(wù)要等待子任務(wù)完成后,父任務(wù)才能繼續(xù)完往下執(zhí)行;而是指父任務(wù)如果先執(zhí)行完畢,那么必須等待子任務(wù)完成后,父任務(wù)才算完成。
這里來(lái)探究 TaskCreationOptions.AttachedToParent
的使用。代碼示例如下:
// 父子任務(wù) Task task = new Task(() => { // TaskCreationOptions.AttachedToParent // 將此任務(wù)附加到父任務(wù)中 // 父任務(wù)需要等待所有子任務(wù)完成后,才能算完成 Task task1 = new Task(() => { Thread.Sleep(TimeSpan.FromSeconds(1)); for (int i = 0; i < 5; i++) { Console.WriteLine(" 內(nèi)層任務(wù)1"); Thread.Sleep(TimeSpan.FromSeconds(0.5)); } }, TaskCreationOptions.AttachedToParent); task1.Start(); Console.WriteLine("最外層任務(wù)"); Thread.Sleep(TimeSpan.FromSeconds(1)); }); task.Start(); task.Wait(); Console.ReadKey();
而 TaskCreationOptions.DenyChildAttach
則不允許其它任務(wù)附加到外層任務(wù)中。
static void Main() { // 不允許出現(xiàn)父子任務(wù) Task task = new Task(() => { Task task1 = new Task(() => { Thread.Sleep(TimeSpan.FromSeconds(1)); for (int i = 0; i < 5; i++) { Console.WriteLine(" 內(nèi)層任務(wù)1"); Thread.Sleep(TimeSpan.FromSeconds(0.5)); } }, TaskCreationOptions.AttachedToParent); task1.Start(); Console.WriteLine("最外層任務(wù)"); Thread.Sleep(TimeSpan.FromSeconds(1)); }, TaskCreationOptions.DenyChildAttach); // 不收兒子 task.Start(); task.Wait(); Console.ReadKey(); }
然后,這里也學(xué)習(xí)了一個(gè)新的 Task 方法:Wait()
等待 Task 完成執(zhí)行過(guò)程。Wait()
也可以設(shè)置超時(shí)時(shí)間。
如果父任務(wù)是通過(guò)調(diào)用 Task.Run 方法而創(chuàng)建的,則可以隱式阻止子任務(wù)附加到其中。
關(guān)于附加的子任務(wù),請(qǐng)參考:https://docs.microsoft.com/zh-cn/dotnet/standard/parallel-programming/attached-and-detached-child-tasks?view=netcore-3.1
任務(wù)返回結(jié)果以及異步獲取返回結(jié)果
要獲取任務(wù)返回結(jié)果,要使用泛型類或方法創(chuàng)建任務(wù),例如 Task<Tresult>
、Task.Factory.StartNew<TResult>()
、Task.Run<TResult>
。
通過(guò) 其泛型的 的 Result
屬性,可以獲得返回結(jié)果。
異步獲取任務(wù)執(zhí)行結(jié)果:
class Program { static void Main() { // ******************************* Task<int> task = new Task<int>(() => { return 666; }); // 執(zhí)行 task.Start(); // 獲取結(jié)果,屬于異步 int number = task.Result; // ******************************* task = Task.Factory.StartNew<int>(() => { return 666; }); // 也可以異步獲取結(jié)果 number = task.Result; // ******************************* task = Task.Run<int>(() => { return 666; }); // 也可以異步獲取結(jié)果 number = task.Result; Console.ReadKey(); } }
如果要同步的話,可以改成:
int number = Task.Factory.StartNew<int>(() => { return 666; }).Result;
捕獲任務(wù)異常
進(jìn)行中的任務(wù)發(fā)生了異常,不會(huì)直接拋出來(lái)阻止主線程執(zhí)行,當(dāng)獲取任務(wù)處理結(jié)果或者等待任務(wù)完成時(shí),異常會(huì)重新拋出。
示例如下:
static void Main() { // ******************************* Task<int> task = new Task<int>(() => { throw new Exception("反正就想彈出一個(gè)異常"); }); // 執(zhí)行 task.Start(); Console.WriteLine("任務(wù)中的異常不會(huì)直接傳播到主線程"); Thread.Sleep(TimeSpan.FromSeconds(1)); // 當(dāng)任務(wù)發(fā)生異常,獲取結(jié)果時(shí)會(huì)彈出 int number = task.Result; // task.Wait(); 等待任務(wù)時(shí),如果發(fā)生異常,也會(huì)彈出 Console.ReadKey(); }
亂拋出異常不是很好的行為噢~可以改成如下:
static void Main() { Task<Program> task = new Task<Program>(() => { try { throw new Exception("反正就想彈出一個(gè)異常"); return new Program(); } catch { return null; } }); task.Start(); var result = task.Result; if (result is null) Console.WriteLine("任務(wù)執(zhí)行失敗"); else Console.WriteLine("任務(wù)執(zhí)行成功"); Console.ReadKey(); }
全局捕獲任務(wù)異常
TaskScheduler.UnobservedTaskException
是一個(gè)事件,其委托定義如下:
public delegate void EventHandler<TEventArgs>(object? sender, TEventArgs e);
下面是一個(gè)示例:
請(qǐng)發(fā)布程序后,打開(kāi)目錄執(zhí)行程序。
class Program { static void Main() { TaskScheduler.UnobservedTaskException += MyTaskException; Task.Factory.StartNew(() => { throw new ArgumentNullException(); }); Thread.Sleep(100); GC.Collect(); GC.WaitForPendingFinalizers(); Console.WriteLine("Done"); Console.ReadKey(); } public static void MyTaskException(object sender, UnobservedTaskExceptionEventArgs eventArgs) { // eventArgs.SetObserved(); ((AggregateException)eventArgs.Exception).Handle(ex => { Console.WriteLine("Exception type: {0}", ex.GetType()); return true; }); } }
TaskScheduler.UnobservedTaskException 到底怎么用,筆者不太清楚。而且效果難以觀察。
請(qǐng)參考:
https://stackoverflow.com/search?q=TaskScheduler.UnobservedTaskException
到此這篇關(guān)于C#多線程系列之任務(wù)基礎(chǔ)(一)的文章就介紹到這了。希望對(duì)大家的學(xué)習(xí)有所幫助,也希望大家多多支持腳本之家。
相關(guān)文章
C#字節(jié)數(shù)組(byte[])和字符串相互轉(zhuǎn)換方式
這篇文章主要介紹了C#字節(jié)數(shù)組(byte[])和字符串相互轉(zhuǎn)換方式,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2023-02-02c# 動(dòng)態(tài)加載dll文件,并實(shí)現(xiàn)調(diào)用其中的簡(jiǎn)單方法
下面小編就為大家?guī)?lái)一篇c# 動(dòng)態(tài)加載dll文件,并實(shí)現(xiàn)調(diào)用其中的簡(jiǎn)單方法。小編覺(jué)得挺不錯(cuò)的,現(xiàn)在就分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧2017-01-01C#?Unity使用正則表達(dá)式去除部分富文本的代碼示例
正則表達(dá)式在我們?nèi)粘i_(kāi)發(fā)中的用處不用多說(shuō)了吧,下面這篇文章主要給大家介紹了關(guān)于C#?Unity使用正則表達(dá)式去除部分富文本的相關(guān)資料,文中通過(guò)實(shí)例代碼介紹的非常詳細(xì),需要的朋友可以參考下2023-03-03聚星C#數(shù)字信號(hào)處理工具包頻譜分析的用法
這篇文章主要介紹了聚星C#數(shù)字信號(hào)處理工具包頻譜分析的用法,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2023-02-02C# 解決在Dictionary中使用枚舉的效率問(wèn)題
這篇文章主要介紹了C# 解決在Dictionary中使用枚舉的效率問(wèn)題,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過(guò)來(lái)看看吧2021-04-04