同步和異步主要用于修飾方法。當一個(gè)方法被調用時(shí),調用者需要等待該方法執行完畢并返回才能繼續執行,我們稱(chēng)這個(gè)方法是同步方法;當一個(gè)方法被調用時(shí)立即返回,并獲取一個(gè)線(xiàn)程執行該方法內部的業(yè)務(wù),調用者不用等待該方法執行完畢,我們稱(chēng)這個(gè)方法為異步方法。
異步的好處在于非阻塞(調用線(xiàn)程不會(huì )暫停執行去等待子線(xiàn)程完成),因此我們把一些不需要立即使用結果、較耗時(shí)的任務(wù)設為異步執行,可以提高程序的運行效率。net4.0在ThreadPool的基礎上推出了Task類(lèi),微軟極力推薦使用Task來(lái)執行異步任務(wù),現在C#類(lèi)庫中的異步方法基本都用到了Task;net5.0推出了async/await,讓異步編程更為方便。本篇主要介紹Task、async/await相關(guān)的內容,其他異步操作的方式會(huì )在下一篇介紹。
回到頂部
Task是在ThreadPool的基礎上推出的,我們簡(jiǎn)單了解下ThreadPool。ThreadPool中有若干數量的線(xiàn)程,如果有任務(wù)需要處理時(shí),會(huì )從線(xiàn)程池中獲取一個(gè)空閑的線(xiàn)程來(lái)執行任務(wù),任務(wù)執行完畢后線(xiàn)程不會(huì )銷(xiāo)毀,而是被線(xiàn)程池回收以供后續任務(wù)使用。當線(xiàn)程池中所有的線(xiàn)程都在忙碌時(shí),又有新任務(wù)要處理時(shí),線(xiàn)程池才會(huì )新建一個(gè)線(xiàn)程來(lái)處理該任務(wù),如果線(xiàn)程數量達到設置的最大值,任務(wù)會(huì )排隊,等待其他任務(wù)釋放線(xiàn)程后再執行。線(xiàn)程池能減少線(xiàn)程的創(chuàng )建,節省開(kāi)銷(xiāo),看一個(gè)ThreadPool的栗子吧
static void Main(string[] args) { for (int i = 1; i <=10; i++) { //ThreadPool執行任務(wù) ThreadPool.QueueUserWorkItem(new WaitCallback((obj) => { Console.WriteLine($"第{obj}個(gè)執行任務(wù)"); }),i); } Console.ReadKey(); }
上邊的代碼通過(guò)ThreadPool執行了10個(gè)任務(wù),執行結果為:
ThreadPool相對于Thread來(lái)說(shuō)可以減少線(xiàn)程的創(chuàng )建,有效減小系統開(kāi)銷(xiāo);但是ThreadPool不能控制線(xiàn)程的執行順序,我們也不能獲取線(xiàn)程池內線(xiàn)程取消/異常/完成的通知,即我們不能有效監控和控制線(xiàn)程池中的線(xiàn)程。
我們知道了ThreadPool的弊端:我們不能控制線(xiàn)程池中線(xiàn)程的執行順序,也不能獲取線(xiàn)程池內線(xiàn)程取消/異常/完成的通知。net4.0在ThreadPool的基礎上推出了Task,Task擁有線(xiàn)程池的優(yōu)點(diǎn),同時(shí)也解決了使用線(xiàn)程池不易控制的弊端。
首先看一下怎么去創(chuàng )建并運行一個(gè)Task,Task的創(chuàng )建和執行方式有如下三種:
static void Main(string[] args) { //1.new方式實(shí)例化一個(gè)Task,需要通過(guò)Start方法啟動(dòng) Task task = new Task(() => { Thread.Sleep(100); Console.WriteLine($"hello, task1的線(xiàn)程ID為{Thread.CurrentThread.ManagedThreadId}"); }); task.Start();
//2.Task.Factory.StartNew(Action action)創(chuàng )建和啟動(dòng)一個(gè)Task Task task2 = Task.Factory.StartNew(() => { Thread.Sleep(100); Console.WriteLine($"hello, task2的線(xiàn)程ID為{ Thread.CurrentThread.ManagedThreadId}"); });
//3.Task.Run(Action action)將任務(wù)放在線(xiàn)程池隊列,返回并啟動(dòng)一個(gè)Task Task task3 = Task.Run(() => { Thread.Sleep(100); Console.WriteLine($"hello, task3的線(xiàn)程ID為{ Thread.CurrentThread.ManagedThreadId}"); }); Console.WriteLine("執行主線(xiàn)程!"); Console.ReadKey(); }
執行結果如下:
我們看到先打印"執行主線(xiàn)程",然后再打印各個(gè)任務(wù),說(shuō)明了Task不會(huì )阻塞主線(xiàn)程。上邊的栗子Task都沒(méi)有返回值,我們也可以創(chuàng )建有返回值的Task<TResult>,用法和沒(méi)有返回值的基本一致,我們簡(jiǎn)單修改一下上邊的栗子,代碼如下:
static void Main(string[] args) { ////1.new方式實(shí)例化一個(gè)Task,需要通過(guò)Start方法啟動(dòng) Task<string> task = new Task<string>(() => { return $"hello, task1的ID為{Thread.CurrentThread.ManagedThreadId}"; }); task.Start();
////2.Task.Factory.StartNew(Func func)創(chuàng )建和啟動(dòng)一個(gè)Task Task<string> task2 =Task.Factory.StartNew<string>(() => { return $"hello, task2的ID為{ Thread.CurrentThread.ManagedThreadId}"; });
////3.Task.Run(Func func)將任務(wù)放在線(xiàn)程池隊列,返回并啟動(dòng)一個(gè)Task Task<string> task3= Task.Run<string>(() => { return $"hello, task3的ID為{ Thread.CurrentThread.ManagedThreadId}"; });
Console.WriteLine("執行主線(xiàn)程!"); Console.WriteLine(task.Result); Console.WriteLine(task2.Result); Console.WriteLine(task3.Result); Console.ReadKey(); }
注意task.Resut獲取結果時(shí)會(huì )阻塞線(xiàn)程,即如果task沒(méi)有執行完成,會(huì )等待task執行完成獲取到Result,然后再執行后邊的代碼,程序運行結果如下:
上邊的所有栗子中Task的執行都是異步的,不會(huì )阻塞主線(xiàn)程。有些場(chǎng)景下我們想讓Task同步執行怎么辦呢?Task提供了 task.RunSynchronously()用于同步執行Task任務(wù),代碼如下:
static void Main(string[] args) { Task task = new Task(() => { Thread.Sleep(100); Console.WriteLine("執行Task結束!"); }); //同步執行,task會(huì )阻塞主線(xiàn)程 task.RunSynchronously(); Console.WriteLine("執行主線(xiàn)程結束!"); Console.ReadKey(); }
執行結果如下:
使用Thread時(shí),我們知道用thread.Join()方法即可阻塞主線(xiàn)程??匆粋€(gè)例子:
static void Main(string[] args) { Thread th1 = new Thread(() => { Thread.Sleep(500); Console.WriteLine("線(xiàn)程1執行完畢!"); }); th1.Start(); Thread th2 = new Thread(() => { Thread.Sleep(1000); Console.WriteLine("線(xiàn)程2執行完畢!"); }); th2.Start(); //阻塞主線(xiàn)程 th1.Join(); th2.Join(); Console.WriteLine("主線(xiàn)程執行完畢!"); Console.ReadKey(); }
如果注釋掉兩個(gè)Join,執行結果是:先打印【主線(xiàn)程執行完畢】,而添加兩個(gè)Join方法后執行結果如下,實(shí)現了線(xiàn)程阻塞:
Thread的Join方法可以阻塞調用線(xiàn)程,但是有一些弊端:①如果我們要實(shí)現很多線(xiàn)程的阻塞時(shí),每個(gè)線(xiàn)程都要調用一次Join方法;②如果我們想讓所有的線(xiàn)程執行完畢(或者任一線(xiàn)程執行完畢)時(shí),立即解除阻塞,使用Join方法不容易實(shí)現。Task提供了 Wait/WaitAny/WaitAll 方法,可以更方便地控制線(xiàn)程阻塞。
task.Wait() 表示等待task執行完畢,功能類(lèi)似于thead.Join(); Task.WaitAll(Task[] tasks) 表示只有所有的task都執行完成了再解除阻塞; Task.WaitAny(Task[] tasks) 表示只要有一個(gè)task執行完畢就解除阻塞,看一個(gè)栗子:


執行結果如下:

如果將栗子中的WaitAll換成WaitAny,那么任一task執行完畢就會(huì )解除線(xiàn)程阻塞,執行結果是:先打印【線(xiàn)程1執行完畢】,然后打印【主線(xiàn)程執行完畢】,最后打印【線(xiàn)程2執行完畢】
上邊的Wait/WaitAny/WaitAll方法返回值為void,這些方法單純的實(shí)現阻塞線(xiàn)程。我們現在想讓所有task執行完畢(或者任一task執行完畢)后,開(kāi)始執行后續操作,怎么實(shí)現呢?這時(shí)就可以用到WhenAny/WhenAll方法了,這些方法執行完成返回一個(gè)task實(shí)例。 task.WhenAll(Task[] tasks) 表示所有的task都執行完畢后再去執行后續的操作, task.WhenAny(Task[] tasks) 表示任一task執行完畢后就開(kāi)始執行后續操作??匆粋€(gè)栗子:
static void Main(string[] args) { Task task1 = new Task(() => { Thread.Sleep(500); Console.WriteLine("線(xiàn)程1執行完畢!"); }); task1.Start(); Task task2 = new Task(() => { Thread.Sleep(1000); Console.WriteLine("線(xiàn)程2執行完畢!"); }); task2.Start(); //task1,task2執行完了后執行后續操作 Task.WhenAll(task1, task2).ContinueWith((t) => { Thread.Sleep(100); Console.WriteLine("執行后續操作完畢!"); });
Console.WriteLine("主線(xiàn)程執行完畢!"); Console.ReadKey(); }
執行結果如下,我們看到WhenAll/WhenAny方法不會(huì )阻塞主線(xiàn)程,當使用WhenAll方法時(shí)所有的task都執行完畢才會(huì )執行后續操作;如果把栗子中的WhenAll替換成WhenAny,則只要有一個(gè)線(xiàn)程執行完畢就會(huì )開(kāi)始執行后續操作,這里不再演示。

上邊的栗子也可以通過(guò) Task.Factory.ContinueWhenAll(Task[] tasks, Action continuationAction) 和 Task.Factory.ContinueWhenAny(Task[] tasks, Action continuationAction) 來(lái)實(shí)現 ,修改上邊栗子代碼如下,執行結果不變。
static void Main(string[] args) { Task task1 = new Task(() => { Thread.Sleep(500); Console.WriteLine("線(xiàn)程1執行完畢!"); }); task1.Start(); Task task2 = new Task(() => { Thread.Sleep(1000); Console.WriteLine("線(xiàn)程2執行完畢!"); }); task2.Start(); //通過(guò)TaskFactroy實(shí)現 Task.Factory.ContinueWhenAll(new Task[] { task1, task2 }, (t) => { Thread.Sleep(100); Console.WriteLine("執行后續操作"); });
Console.WriteLine("主線(xiàn)程執行完畢!"); Console.ReadKey(); }
在Task前我們執行任務(wù)采用的是Thread,Thread怎么取消任務(wù)呢?一般流程是:設置一個(gè)變量來(lái)控制任務(wù)是否停止,如設置一個(gè)變量isStop,然后線(xiàn)程輪詢(xún)查看isStop,如果isStop為true就停止,代碼如下:
static void Main(string[] args) { bool isStop = false; int index = 0; //開(kāi)啟一個(gè)線(xiàn)程執行任務(wù) Thread th1 = new Thread(() => { while (!isStop) { Thread.Sleep(1000); Console.WriteLine($"第{++index}次執行,線(xiàn)程運行中..."); } }); th1.Start(); //五秒后取消任務(wù)執行 Thread.Sleep(5000); isStop = true; Console.ReadKey(); }
Task中有一個(gè)專(zhuān)門(mén)的類(lèi) CancellationTokenSource 來(lái)取消任務(wù)執行,還是使用上邊的例子,我們修改代碼如下,程序運行的效果不變。


CancellationTokenSource的功能不僅僅是取消任務(wù)執行,我們可以使用 source.CancelAfter(5000) 實(shí)現5秒后自動(dòng)取消任務(wù),也可以通過(guò) source.Token.Register(Action action) 注冊取消任務(wù)觸發(fā)的回調函數,即任務(wù)被取消時(shí)注冊的action會(huì )被執行??匆粋€(gè)栗子:
static void Main(string[] args) { CancellationTokenSource source = new CancellationTokenSource(); //注冊任務(wù)取消的事件 source.Token.Register(() => { Console.WriteLine("任務(wù)被取消后執行xx操作!"); });
int index = 0; //開(kāi)啟一個(gè)task執行任務(wù) Task task1 = new Task(() => { while (!source.IsCancellationRequested) { Thread.Sleep(1000); Console.WriteLine($"第{++index}次執行,線(xiàn)程運行中..."); } }); task1.Start(); //延時(shí)取消,效果等同于Thread.Sleep(5000);source.Cancel(); source.CancelAfter(5000); Console.ReadKey(); }
執行結果如下,第5次執行在取消回調后打印,這是因為,執行取消的時(shí)候第5次任務(wù)已經(jīng)通過(guò)了while()判斷,任務(wù)已經(jīng)執行中了:

最后看上一篇跨線(xiàn)程的栗子,點(diǎn)擊按鈕啟動(dòng)一個(gè)任務(wù),給tetxtbox賦值,我們把Thread改成Task,代碼如下:
public partial class Form1 : Form { public Form1() { InitializeComponent(); }
private void mySetValueBtn_Click(object sender, EventArgs e) { Task.Run(() => { Action<int> setValue = (i) => { myTxtbox.Text = i.ToString(); }; for (int i = 0; i < 1000000; i++) { myTxtbox.Invoke(setValue,i); } }); } }
運行界面如下,賦值的task不會(huì )阻塞UI線(xiàn)程:

回到頂部
在C#5.0中出現的 async和await ,讓異步編程變得更簡(jiǎn)單。我們看一個(gè)獲取文件內容的栗子:
class Program { static void Main(string[] args) { string content = GetContentAsync(Environment.CurrentDirectory + @"/test.txt").Result; //調用同步方法 //string content = GetContent(Environment.CurrentDirectory + @"/test.txt"); Console.WriteLine(content); Console.ReadKey(); } //異步讀取文件內容 async static Task<string> GetContentAsync(string filename) { FileStream fs = new FileStream(filename, FileMode.Open); var bytes = new byte[fs.Length]; //ReadAync方法異步讀取內容,不阻塞線(xiàn)程 Console.WriteLine("開(kāi)始讀取文件"); int len = await fs.ReadAsync(bytes, 0, bytes.Length); string result = Encoding.UTF8.GetString(bytes); return result; } //同步讀取文件內容 static string GetContent(string filename) { FileStream fs = new FileStream(filename, FileMode.Open); var bytes = new byte[fs.Length]; //Read方法同步讀取內容,阻塞線(xiàn)程 int len = fs.Read(bytes, 0, bytes.Length); string result = Encoding.UTF8.GetString(bytes); return result; } }
test.txt內容是【hello world!】執行結果為:

上邊的栗子也寫(xiě)出了同步讀取的方式,將main函數中的注釋去掉即可同步讀取文件內容。我們可以看到異步讀取代碼和同步讀取代碼基本一致。async/await讓異步編碼變得更簡(jiǎn)單,我們可以像寫(xiě)同步代碼一樣去寫(xiě)異步代碼。注意一個(gè)小問(wèn)題:異步方法中方法簽名返回值為T(mén)ask<T>,代碼中的返回值為T(mén)。上邊栗子中GetContentAsync的簽名返回值為T(mén)ask<string>,而代碼中返回值為string。牢記這一細節對我們分析異步代碼很有幫助。
異步方法簽名的返回值有以下三種:
?、?Task<T>:如果調用方法想通過(guò)調用異步方法獲取一個(gè)T類(lèi)型的返回值,那么簽名必須為T(mén)ask<TResult>;
?、?Task:如果調用方法不想通過(guò)異步方法獲取一個(gè)值,僅僅想追蹤異步方法的執行狀態(tài),那么我們可以設置異步方法簽名的返回值為T(mén)ask;
?、?void:如果調用方法僅僅只是調用一下異步方法,不和異步方法做其他交互,我們可以設置異步方法簽名的返回值為void,這種形式也叫做“調用并忘記”。
小結:到這里Task,async/await的簡(jiǎn)單使用已經(jīng)基本結束了,一些高級特性等到工作遇到了再去研究。通過(guò)上邊的介紹,我們知道async/await是基于Task的,而Task是對ThreadPool的封裝改進(jìn),主要是為了更有效的控制線(xiàn)程池中的線(xiàn)程(ThreadPool中的線(xiàn)程,我們很難通過(guò)代碼控制其執行順序,任務(wù)延續和取消等等);ThreadPool基于Thread的,主要目的是減少Thread創(chuàng )建數量和管理Thread的成本。async/await Task是C#中更先進(jìn)的,也是微軟大力推廣的特性,我們在開(kāi)發(fā)中可以嘗試使用Task來(lái)替代Thread/ThreadPool,處理本地IO和網(wǎng)絡(luò )IO任務(wù)是盡量使用async/await來(lái)提高任務(wù)執行效率。
聯(lián)系客服