今天就跟大家聊聊有關(guān).NET中異步和多線程的應(yīng)用,可能很多人都不太了解,為了讓大家更加了解,小編給大家總結(jié)了以下內(nèi)容,希望大家根據(jù)這篇文章可以有所收獲。
創(chuàng)新互聯(lián)公司長(zhǎng)期為上千家客戶提供的網(wǎng)站建設(shè)服務(wù),團(tuán)隊(duì)從業(yè)經(jīng)驗(yàn)10年,關(guān)注不同地域、不同群體,并針對(duì)不同對(duì)象提供差異化的產(chǎn)品和服務(wù);打造開(kāi)放共贏平臺(tái),與合作伙伴共同營(yíng)造健康的互聯(lián)網(wǎng)生態(tài)環(huán)境。為洪澤企業(yè)提供專業(yè)的網(wǎng)站建設(shè)、成都做網(wǎng)站,洪澤網(wǎng)站改版等技術(shù)服務(wù)。擁有10年豐富建站經(jīng)驗(yàn)和眾多成功案例,為您定制開(kāi)發(fā)。
線程(英文:thread),操作系統(tǒng)技術(shù)中的術(shù)語(yǔ),是操作系統(tǒng)能夠進(jìn)行運(yùn)算調(diào)度的最小單位,它被包涵在進(jìn)程之中,是行程中的實(shí)際運(yùn)作單位。
Async是異步串行端口,主要應(yīng)用于Modem或Modem池的連接。它主要用于實(shí)現(xiàn)遠(yuǎn)程計(jì)算機(jī)通過(guò)公用電話撥入網(wǎng)絡(luò),數(shù)據(jù)速率不高,不要求通信設(shè)備之間保持同步。
一、任務(wù)Task
System.Threading.Tasks在.NET4引入,前面線程的API太多了,控制不方便,而ThreadPool控制能力又太弱,比如做線程的延續(xù)、阻塞、取消、超時(shí)等功能不太方便,所以Task就抽象了線程功能,在后臺(tái)使用ThreadPool
1、啟動(dòng)任務(wù)
可以使用TaskFactory類或Task類的構(gòu)造函數(shù)和Start()方法,委托可以提供帶有一個(gè)Object類型的輸入?yún)?shù),所以可以給任務(wù)傳遞任意數(shù)據(jù),還漏了一個(gè)常用的Task.Run
TaskFactory taskFactory = new TaskFactory(); taskFactory.StartNew(() => { Console.WriteLine($"tid={Thread.CurrentThread.ManagedThreadId},datetime={DateTime.Now}"); }); Task.Factory.StartNew(() => { Console.WriteLine($"tid={Thread.CurrentThread.ManagedThreadId},datetime={DateTime.Now}"); }); Task task = new Task(() => { Console.WriteLine($"tid={Thread.CurrentThread.ManagedThreadId},datetime={DateTime.Now}"); }); task.Start();
只有Task類實(shí)例方式需要Start()去啟動(dòng)任務(wù),當(dāng)然可以RunSynchronously()來(lái)同步執(zhí)行任務(wù),主線程會(huì)等待,就是用主線程來(lái)執(zhí)行這個(gè)task任務(wù)
Task task = new Task(() => { Thread.Sleep(10000); Console.WriteLine($"tid={Thread.CurrentThread.ManagedThreadId},datetime={DateTime.Now}"); }); task.RunSynchronously();
2、阻塞延續(xù)
在Thread中我們使用join來(lái)阻塞等待,在多個(gè)Thread時(shí)進(jìn)行控制就不太方便。Task中我們使用實(shí)例方法Wait阻塞單個(gè)任務(wù)或靜態(tài)方法WaitAll和WaitAny阻塞多個(gè)任務(wù)
var task = new Task(() => { Thread.Sleep(5*1000); Console.WriteLine($"tid={Thread.CurrentThread.ManagedThreadId},datetime={DateTime.Now}"); }); var task2 = new Task(() => { Thread.Sleep(10 * 1000); Console.WriteLine($"tid={Thread.CurrentThread.ManagedThreadId},datetime={DateTime.Now}"); }); task.Start(); task2.Start(); //task.Wait();//單任務(wù)等待 //Task.WaitAny(task, task2);//任何一個(gè)任務(wù)完成就繼續(xù) Task.WaitAll(task, task2);//任務(wù)都完成才繼續(xù)
如果不希望阻塞主線程,實(shí)現(xiàn)當(dāng)一個(gè)任務(wù)或幾個(gè)任務(wù)完成后執(zhí)行別的任務(wù),可以使用Task靜態(tài)方法WhenAll和WhenAny,他們將返回一個(gè)Task,但這個(gè)Task不允許你控制,將會(huì)在滿足WhenAll和WhenAny里任務(wù)完成時(shí)自動(dòng)完成,然后調(diào)用Task的ContinueWith方法,就可以在一個(gè)任務(wù)完成后緊跟開(kāi)始另一個(gè)任務(wù)
Task.WhenAll(task, task2).ContinueWith((t) => { Console.WriteLine($"tid={Thread.CurrentThread.ManagedThreadId},datetime={DateTime.Now}"); }); Task.Factory工廠中也存在類似ContinueWhenAll和ContinueWhenAny
3、任務(wù)層次結(jié)構(gòu)
不僅可以在一個(gè)任務(wù)結(jié)束后執(zhí)行另一個(gè)任務(wù),也可以在一個(gè)任務(wù)內(nèi)啟動(dòng)一個(gè)任務(wù),這就啟動(dòng)了一個(gè)父子層次結(jié)構(gòu)
var parentTask = new Task(()=> { Console.WriteLine($"parentId={Thread.CurrentThread.ManagedThreadId},datetime={DateTime.Now}"); Thread.Sleep(5*1000); var childTask = new Task(() => { Thread.Sleep(10 * 1000); Console.WriteLine($"childId={Thread.CurrentThread.ManagedThreadId},datetime={DateTime.Now}") }); childTask.Start(); }); parentTask.Start();
如果父任務(wù)在子任務(wù)之前結(jié)束,父任務(wù)的狀態(tài)為WaitingForChildrenToComplete,當(dāng)子任務(wù)也完成時(shí),父任務(wù)的狀態(tài)就變?yōu)镽anToCompletion,當(dāng)然,在創(chuàng)建任務(wù)時(shí)指定TaskCreationOptions枚舉參數(shù),可以控制任務(wù)的創(chuàng)建和執(zhí)行的可選行為
4、枚舉參數(shù)
簡(jiǎn)單介紹下創(chuàng)建任務(wù)中的TaskCreationOptions枚舉參數(shù),創(chuàng)建任務(wù)時(shí)我們可以提供TaskCreationOptions枚舉參數(shù),用于控制任務(wù)的創(chuàng)建和執(zhí)行的可選行為的標(biāo)志
AttachedToParent:指定將任務(wù)附加到任務(wù)層次結(jié)構(gòu)中的某個(gè)父級(jí),意思就是建立父子關(guān)系,父任務(wù)必須等待子任務(wù)完成才可以繼續(xù)執(zhí)行。和WaitAll效果一樣。上面例子如果在創(chuàng)建子任務(wù)時(shí)指定TaskCreationOptions.AttachedToParent,那么父任務(wù)wait時(shí)也會(huì)等子任務(wù)的結(jié)束
DenyChildAttach:不讓子任務(wù)附加到父任務(wù)上
LongRunning:指定是長(zhǎng)時(shí)間運(yùn)行任務(wù),如果事先知道這個(gè)任務(wù)會(huì)耗時(shí)比較長(zhǎng),建議設(shè)置此項(xiàng)。這樣,Task調(diào)度器會(huì)創(chuàng)建Thread線程,而不使用ThreadPool線程。因?yàn)槟汩L(zhǎng)時(shí)間占用ThreadPool線程不還,那它可能必要時(shí)會(huì)在線程池中開(kāi)啟新的線程,造成調(diào)度壓力
PreferFairness:盡可能公平的安排任務(wù),這意味著較早安排的任務(wù)將更可能較早運(yùn)行,而較晚安排運(yùn)行的任務(wù)將更可能較晚運(yùn)行。實(shí)際通過(guò)把任務(wù)放到線程池的全局隊(duì)列中,讓工作線程去爭(zhēng)搶,默認(rèn)是在本地隊(duì)列中。
另一個(gè)枚舉參數(shù)是ContinueWith方法中的TaskContinuationOptions枚舉參數(shù),它除了擁有幾個(gè)和上面同樣功能的枚舉值外,還擁有控制任務(wù)的取消延續(xù)等功能
LazyCancellation:在延續(xù)取消的情況下,防止延續(xù)的完成直到完成先前的任務(wù)。什么意思呢?
CancellationTokenSource source = new CancellationTokenSource(); source.Cancel(); var task1 = new Task(() => { Console.WriteLine($"task1 id={Thread.CurrentThread.ManagedThreadId},datetime={DateTime.Now}"); }); var task2 = task1.ContinueWith(t => { Console.WriteLine($"task2 id={Thread.CurrentThread.ManagedThreadId},datetime={DateTime.Now}"); },source.Token); var task3 = task2.ContinueWith(t => { Console.WriteLine($"task3 id={Thread.CurrentThread.ManagedThreadId},datetime={DateTime.Now}"); }); task1.Start();
上面例子我們企圖task1->task2->task3順序執(zhí)行,然后通過(guò)CancellationToken來(lái)取消task2的執(zhí)行。結(jié)果會(huì)是怎樣呢?結(jié)果task1和task3會(huì)并行執(zhí)行(task3也是會(huì)執(zhí)行的,而且是和task1并行,等于原來(lái)的一條鏈變成了兩條鏈),然后我們嘗試使用
LazyCancellation, var task2 = task1.ContinueWith(t => { Console.WriteLine($"task2 id={Thread.CurrentThread.ManagedThreadId},datetime={DateTime.Now}"); },source.Token,TaskContinuationOptions.LazyCancellation,TaskScheduler.Current);
這樣,將會(huì)在task1執(zhí)行完成后,task2才去判斷source.Token,為Cancel就不執(zhí)行,接下來(lái)執(zhí)行task3就保證了原來(lái)的順序
ExecuteSynchronously:指定應(yīng)同步執(zhí)行延續(xù)任務(wù),比如上例中,在延續(xù)任務(wù)task2中指定此參數(shù),則task2會(huì)使用執(zhí)行task1的線程來(lái)執(zhí)行,這樣防止線程切換,可以做些共有資源的訪問(wèn)。不指定的話就隨機(jī),但也能也用到task1的線程
NotOnRanToCompletion:延續(xù)任務(wù)必須在前面任務(wù)非完成狀態(tài)下執(zhí)行
OnlyOnRanToCompletion:延續(xù)任務(wù)必須在前面任務(wù)完成狀態(tài)才能執(zhí)行
NotOnFaulted,OnlyOnCanceled,OnlyOnFaulted等等
5、任務(wù)取消
在上篇使用Thread時(shí),我們使用一個(gè)變量isStop標(biāo)記是否取消任務(wù),這種訪問(wèn)共享變量的方式難免會(huì)出問(wèn)題。task中提出CancellationTokenSource類專門處理任務(wù)取消,常見(jiàn)用法看下面代碼注釋
CancellationTokenSource source = new CancellationTokenSource();//構(gòu)造函數(shù)中也可指定延遲取消 //注冊(cè)一個(gè)取消時(shí)調(diào)用的委托 source.Token.Register(() => { Console.WriteLine("當(dāng)前source已經(jīng)取消,可以在這里做一些其他事情(比如資源清理)..."); }); var task1 = new Task(() => { while (!source.IsCancellationRequested) { Console.WriteLine($"task1 id={Thread.CurrentThread.ManagedThreadId},datetime={DateTime.Now}"); } },source.Token); task1.Start(); //source.Cancel();//取消 source.CancelAfter(1000);//延時(shí)取消
6、任務(wù)結(jié)果
讓子線程返回結(jié)果,可以將信息寫入到線程安全的共享變量中去,或則使用可以返回結(jié)果的任務(wù)。使用Task的泛型版本Task
var task = new Task(() => { return "hello ketty"; }); task.Start(); string result = task.Result;
7、異常
可以使用AggregateException來(lái)接受任務(wù)中的異常信息,這是一個(gè)聚合異常繼承自Exception,可以遍歷獲取包含的所有異常,以及進(jìn)行異常處理,決定是否繼續(xù)往上拋異常等
var task = Task.Factory.StartNew(() => { var childTask1 = Task.Factory.StartNew(() => { throw new Exception("childTask1異常..."); },TaskCreationOptions.AttachedToParent); var childTask12= Task.Factory.StartNew(() => { throw new Exception("childTask2異常..."); }, TaskCreationOptions.AttachedToParent); }); try { try { task.Wait(); } catch (AggregateException ex) { foreach (var item in ex.InnerExceptions) { Console.WriteLine($"message{item.InnerException.Message}"); } ex.Handle(x => { if (x.InnerException.Message == "childTask1異常...") { return true;//異常被處理,不繼續(xù)往上拋了 } return false; }); } } catch (Exception ex) { throw; }
二、并行Parallel
1、Parallel.For()、Parallel.ForEach()
在.NET4中,另一個(gè)新增的抽象的線程時(shí)Parallel類。這個(gè)類定義了并行的for和foreach的靜態(tài)方法。Parallel.For()和Parallel.ForEach()方法多次調(diào)用一個(gè)方法,而Parallel.Invoke()方法允許同時(shí)調(diào)用不同的方法。首先Parallel是會(huì)阻塞主線程的,它將讓主線程也參與到任務(wù)中
Parallel.For()類似于for允許語(yǔ)句,并行迭代同一個(gè)方法,迭代順序沒(méi)有保證的
ParallelLoopResult result = Parallel.For(0, 10, i => { Console.WriteLine($"{i} task:{Task.CurrentId} thread:{Thread.CurrentThread.ManagedThreadId}"); }); Console.WriteLine(result.IsCompleted);
也可以提前中斷Parallel.For()方法。For()方法的一個(gè)重載版本接受Action
ParallelLoopResult result = Parallel.For(0, 10, new ParallelOptions() { MaxDegreeOfParallelism = 8 },(i,loop) => { Console.WriteLine($"{i} task:{Task.CurrentId} thread:{Thread.CurrentThread.ManagedThreadId}"); if (i > 5) { loop.Break(); } });
2、Parallel.For
For還有一個(gè)高級(jí)泛型版本,相當(dāng)于并行的聚合計(jì)算
ParallelLoopResult For(int fromInclusive, int toExclusive, Func localInit, Func body, Action localFinally);
像下面這樣我們求0…100的和,第三個(gè)參數(shù)更定一個(gè)種子初始值,第四個(gè)參數(shù)迭代累計(jì),最后聚合
int totalNum = 0; Parallel.For(0, 100, () => { return 0; }, (current, loop, total) => { total += current; return total; }, (total) => { Interlocked.Add(ref totalNum, total); });
上面For用來(lái)處理數(shù)組數(shù)據(jù),F(xiàn)orEach()方法用來(lái)處理非數(shù)組的數(shù)據(jù)任務(wù),比如字典數(shù)據(jù)繼承自IEnumerable的集合等
3、Parallel.Invoke()
Parallel.Invoke()則可以并行調(diào)用不同的方法,參數(shù)傳遞一個(gè)Action的委托數(shù)組
Parallel.Invoke(() => { Console.WriteLine($"方法1 thread:{Thread.CurrentThread.ManagedThreadId}"); } , () => { Console.WriteLine($"方法2 thread:{Thread.CurrentThread.ManagedThreadId}"); } , () => { Console.WriteLine($"方法3 thread:{Thread.CurrentThread.ManagedThreadId}"); });
4、PLinq
Plinq,為了能夠達(dá)到最大的靈活度,linq有了并行版本。使用也很簡(jiǎn)單,只需要將原始集合AsParallel就轉(zhuǎn)換為支持并行化的查詢。也可以AsOrdered來(lái)順序執(zhí)行,取消Token,強(qiáng)制并行等
var nums = Enumerable.Range(0, 100); var query = from n in nums.AsParallel() select new { thread=$"tid={Thread.CurrentThread.ManagedThreadId},datetime={DateTime.Now}" };
三、異步等待AsyncAwait
異步編程模型,可能還需要大篇幅來(lái)學(xué)習(xí),這里先介紹下基本用法,內(nèi)在本質(zhì)需要用ILSpy反編譯來(lái)看,以后可能要分專題總結(jié)。文末先給幾個(gè)參考資料,有興趣自己闊以先琢磨琢磨鴨
1、簡(jiǎn)單使用
這是.NET4.5開(kāi)始提供的一對(duì)語(yǔ)法糖,使得可以較簡(jiǎn)便的使用異步編程。async用在方法定義前面,await只能寫在帶有async標(biāo)記的方法中,任何方法都可以增加async,一般成對(duì)出現(xiàn),只有async沒(méi)有意義,只有await會(huì)報(bào)錯(cuò),請(qǐng)先看下面的示例
private static async void AsyncTest() { //主線程執(zhí)行 Console.WriteLine($"before await ThreadId={Thread.CurrentThread.ManagedThreadId}"); TaskFactory taskFactory = new TaskFactory(); Task task = taskFactory.StartNew(() => { Thread.Sleep(3000); Console.WriteLine($"task ThreadId={Thread.CurrentThread.ManagedThreadId}"); }); await task;//主線程到這里就返回了,執(zhí)行主線程任務(wù) //子線程執(zhí)行,其實(shí)是封裝成委托,在task之后成為回調(diào)(編譯器功能 狀態(tài)機(jī)實(shí)現(xiàn)) 后面相當(dāng)于task.ContinueWith() //這個(gè)回調(diào)的線程是不確定的:可能是主線程 可能是子線程 也可能是其他線程,在winform中是主線程 Console.WriteLine($"after await ThreadId={Thread.CurrentThread.ManagedThreadId}"); }
一般使用async都會(huì)讓方法返回一個(gè)Task的,像下面這樣復(fù)雜一點(diǎn)的
private static async TaskAsyncTest2() { Console.WriteLine($"before await ThreadId={Thread.CurrentThread.ManagedThreadId}"); TaskFactory taskFactory = new TaskFactory(); string x = await taskFactory.StartNew(() => { Thread.Sleep(3000); Console.WriteLine($"task ThreadId={Thread.CurrentThread.ManagedThreadId}"); return "task over"; }); Console.WriteLine($"after await ThreadId={Thread.CurrentThread.ManagedThreadId}"); return x; }
通過(guò)var reslult = AsyncTest2().Result;調(diào)用即可。但注意如果調(diào)用Wait或Result的代碼位于UI線程,Task的實(shí)際執(zhí)行在其他線程,其需要返回UI線程則會(huì)造成死鎖,所以應(yīng)該Async all the way
2、優(yōu)雅
從上面簡(jiǎn)單示例中可以看出異步編程的執(zhí)行邏輯:主線程A邏輯->異步任務(wù)線程B邏輯->主線程C邏輯。
異步方法的返回類型只能是void、Task、Task。示例中異步方法的返回值類型是Task,通常void也不推薦使用,沒(méi)有返回值直接用Task就是
上一篇也大概了解到如果我們要在任務(wù)中更新UI,需要調(diào)用Invoke通知UI線程來(lái)更新,代碼看起來(lái)像下面這樣,在一個(gè)任務(wù)后去更新UI
private void button1_Click(object sender, EventArgs e) { var ResultTask = Task.Run(() => { Thread.Sleep(5000); return "任務(wù)完成"; }); ResultTask.ContinueWith((r)=> { textBox1.Invoke(() => { textBox1.Text = r.Result; }); }); }
如果使用async/await會(huì)看起來(lái)像這樣,是不是優(yōu)雅了許多。以看似同步編程的方式實(shí)現(xiàn)異步
private async void button1_Click(object sender, EventArgs e) { var t = Task.Run(() => { Thread.Sleep(5000); return "任務(wù)完成"; }); textBox1.Text = await t; }
3、最后
在.NET 4.5中引入的Async和Await兩個(gè)新的關(guān)鍵字后,用戶能以一種簡(jiǎn)潔直觀的方式實(shí)現(xiàn)異步編程。甚至都不需要改變代碼的邏輯結(jié)構(gòu),就能將原來(lái)的同步函數(shù)改造為異步函數(shù)。
在內(nèi)部實(shí)現(xiàn)上,Async和Await這兩個(gè)關(guān)鍵字由編譯器轉(zhuǎn)換為狀態(tài)機(jī),通過(guò)System.Threading.Tasks中的并行類實(shí)現(xiàn)代碼的異步執(zhí)行。
看完上述內(nèi)容,你們對(duì).NET中異步和多線程有進(jìn)一步的了解嗎?如果還想了解更多相關(guān)內(nèi)容,歡迎關(guān)注創(chuàng)新互聯(lián)行業(yè)資訊頻道,感謝各位的閱讀。