C#多線程和異步之Task和async/await詳解
一、什么是異步
同步和異步主要用于修飾方法。當(dāng)一個(gè)方法被調(diào)用時(shí),調(diào)用者需要等待該方法執(zhí)行完畢并返回才能繼續(xù)執(zhí)行,我們稱這個(gè)方法是同步方法;當(dāng)一個(gè)方法被調(diào)用時(shí)立即返回,并獲取一個(gè)線程執(zhí)行該方法內(nèi)部的業(yè)務(wù),調(diào)用者不用等待該方法執(zhí)行完畢,我們稱這個(gè)方法為異步方法。
異步的好處在于非阻塞(調(diào)用線程不會(huì)暫停執(zhí)行去等待子線程完成),因此我們把一些不需要立即使用結(jié)果、較耗時(shí)的任務(wù)設(shè)為異步執(zhí)行,可以提高程序的運(yùn)行效率。net4.0在ThreadPool的基礎(chǔ)上推出了Task類,微軟極力推薦使用Task來執(zhí)行異步任務(wù),現(xiàn)在C#類庫(kù)中的異步方法基本都用到了Task;net5.0推出了async/await,讓異步編程更為方便。本篇主要介紹Task、async/await相關(guān)的內(nèi)容,其他異步操作的方式會(huì)在下一篇介紹。
二、Task介紹
Task是在ThreadPool的基礎(chǔ)上推出的,我們簡(jiǎn)單了解下ThreadPool。ThreadPool中有若干數(shù)量的線程,如果有任務(wù)需要處理時(shí),會(huì)從線程池中獲取一個(gè)空閑的線程來執(zhí)行任務(wù),任務(wù)執(zhí)行完畢后線程不會(huì)銷毀,而是被線程池回收以供后續(xù)任務(wù)使用。當(dāng)線程池中所有的線程都在忙碌時(shí),又有新任務(wù)要處理時(shí),線程池才會(huì)新建一個(gè)線程來處理該任務(wù),如果線程數(shù)量達(dá)到設(shè)置的最大值,任務(wù)會(huì)排隊(duì),等待其他任務(wù)釋放線程后再執(zhí)行。線程池能減少線程的創(chuàng)建,節(jié)省開銷,看一個(gè)ThreadPool的栗子吧
- static void Main(string[] args)
- {
- for (int i = 1; i <=10; i++)
- {
- //ThreadPool執(zhí)行任務(wù)
- ThreadPool.QueueUserWorkItem(new WaitCallback((obj) => {
- Console.WriteLine($"第{obj}個(gè)執(zhí)行任務(wù)");
- }),i);
- }
- Console.ReadKey();
- }
上邊的代碼通過ThreadPool執(zhí)行了10個(gè)任務(wù),執(zhí)行結(jié)果為:
ThreadPool相對(duì)于Thread來說可以減少線程的創(chuàng)建,有效減小系統(tǒng)開銷;但是ThreadPool不能控制線程的執(zhí)行順序,我們也不能獲取線程池內(nèi)線程取消/異常/完成的通知,即我們不能有效監(jiān)控和控制線程池中的線程。
1 Task創(chuàng)建和運(yùn)行
我們知道了ThreadPool的弊端:我們不能控制線程池中線程的執(zhí)行順序,也不能獲取線程池內(nèi)線程取消/異常/完成的通知。net4.0在ThreadPool的基礎(chǔ)上推出了Task,Task擁有線程池的優(yōu)點(diǎn),同時(shí)也解決了使用線程池不易控制的弊端。
首先看一下怎么去創(chuàng)建并運(yùn)行一個(gè)Task,Task的創(chuàng)建和執(zhí)行方式有如下三種:
- static void Main(string[] args)
- {
- //1.new方式實(shí)例化一個(gè)Task,需要通過Start方法啟動(dòng)
- Task task = new Task(() =>
- {
- Thread.Sleep(100);
- Console.WriteLine($"hello, task1的線程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的線程ID為{ Thread.CurrentThread.ManagedThreadId}");
- });
- //3.Task.Run(Action action)將任務(wù)放在線程池隊(duì)列,返回并啟動(dòng)一個(gè)Task
- Task task3 = Task.Run(() =>
- {
- Thread.Sleep(100);
- Console.WriteLine($"hello, task3的線程ID為{ Thread.CurrentThread.ManagedThreadId}");
- });
- Console.WriteLine("執(zhí)行主線程!");
- Console.ReadKey();
- }
執(zhí)行結(jié)果如下:
我們看到先打印"執(zhí)行主線程",然后再打印各個(gè)任務(wù),說明了Task不會(huì)阻塞主線程。上邊的栗子Task都沒有返回值,我們也可以創(chuàng)建有返回值的Task
- static void Main(string[] args)
- {
- ////1.new方式實(shí)例化一個(gè)Task,需要通過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ù)放在線程池隊(duì)列,返回并啟動(dòng)一個(gè)Task
- Task<string> task3= Task.Run<string>(() =>
- {
- return $"hello, task3的ID為{ Thread.CurrentThread.ManagedThreadId}";
- });
- Console.WriteLine("執(zhí)行主線程!");
- Console.WriteLine(task.Result);
- Console.WriteLine(task2.Result);
- Console.WriteLine(task3.Result);
- Console.ReadKey();
- }
注意task.Resut獲取結(jié)果時(shí)會(huì)阻塞線程,即如果task沒有執(zhí)行完成,會(huì)等待task執(zhí)行完成獲取到Result,然后再執(zhí)行后邊的代碼,程序運(yùn)行結(jié)果如下:
上邊的所有栗子中Task的執(zhí)行都是異步的,不會(huì)阻塞主線程。有些場(chǎng)景下我們想讓Task同步執(zhí)行怎么辦呢?Task提供了 task.RunSynchronously()用于同步執(zhí)行Task任務(wù),代碼如下:
- static void Main(string[] args)
- {
- Task task = new Task(() =>
- {
- Thread.Sleep(100);
- Console.WriteLine("執(zhí)行Task結(jié)束!");
- });
- //同步執(zhí)行,task會(huì)阻塞主線程
- task.RunSynchronously();
- Console.WriteLine("執(zhí)行主線程結(jié)束!");
- Console.ReadKey();
- }
執(zhí)行結(jié)果如下:
2 Task的阻塞方法(Wait/WaitAll/WaitAny)
1 Thread阻塞線程的方法
使用Thread時(shí),我們知道用thread.Join()方法即可阻塞主線程??匆粋€(gè)例子:
- static void Main(string[] args)
- {
- Thread th1 = new Thread(() => {
- Thread.Sleep(500);
- Console.WriteLine("線程1執(zhí)行完畢!");
- });
- th1.Start();
- Thread th2 = new Thread(() => {
- Thread.Sleep(1000);
- Console.WriteLine("線程2執(zhí)行完畢!");
- });
- th2.Start();
- //阻塞主線程
- th1.Join();
- th2.Join();
- Console.WriteLine("主線程執(zhí)行完畢!");
- Console.ReadKey();
- }
如果注釋掉兩個(gè)Join,執(zhí)行結(jié)果是:先打印【主線程執(zhí)行完畢】,而添加兩個(gè)Join方法后執(zhí)行結(jié)果如下,實(shí)現(xiàn)了線程阻塞:
2 Task的Wait/WaitAny/WaitAll方法
Thread的Join方法可以阻塞調(diào)用線程,但是有一些弊端:①如果我們要實(shí)現(xiàn)很多線程的阻塞時(shí),每個(gè)線程都要調(diào)用一次Join方法;②如果我們想讓所有的線程執(zhí)行完畢(或者任一線程執(zhí)行完畢)時(shí),立即解除阻塞,使用Join方法不容易實(shí)現(xiàn)。Task提供了 Wait/WaitAny/WaitAll 方法,可以更方便地控制線程阻塞。
task.Wait() 表示等待task執(zhí)行完畢,功能類似于thead.Join(); Task.WaitAll(Task[] tasks) 表示只有所有的task都執(zhí)行完成了再解除阻塞; Task.WaitAny(Task[] tasks) 表示只要有一個(gè)task執(zhí)行完畢就解除阻塞,看一個(gè)栗子:
- static void Main(string[] args)
- {
- Task task1 = new Task(() => {
- Thread.Sleep(500);
- Console.WriteLine("線程1執(zhí)行完畢!");
- });
- task1.Start();
- Task task2 = new Task(() => {
- Thread.Sleep(1000);
- Console.WriteLine("線程2執(zhí)行完畢!");
- });
- task2.Start();
- //阻塞主線程。task1,task2都執(zhí)行完畢再執(zhí)行主線程
- //執(zhí)行【task1.Wait();task2.Wait();】可以實(shí)現(xiàn)相同功能
- Task.WaitAll(new Task[]{ task1,task2});
- Console.WriteLine("主線程執(zhí)行完畢!");
- Console.ReadKey();
- }
執(zhí)行結(jié)果如下:
如果將栗子中的WaitAll換成WaitAny,那么任一task執(zhí)行完畢就會(huì)解除線程阻塞,執(zhí)行結(jié)果是:先打印【線程1執(zhí)行完畢】,然后打印【主線程執(zhí)行完畢】,最后打印【線程2執(zhí)行完畢】
3 Task的延續(xù)操作(WhenAny/WhenAll/ContinueWith)
上邊的Wait/WaitAny/WaitAll方法返回值為void,這些方法單純的實(shí)現(xiàn)阻塞線程。我們現(xiàn)在想讓所有task執(zhí)行完畢(或者任一task執(zhí)行完畢)后,開始執(zhí)行后續(xù)操作,怎么實(shí)現(xiàn)呢?這時(shí)就可以用到WhenAny/WhenAll方法了,這些方法執(zhí)行完成返回一個(gè)task實(shí)例。 task.WhenAll(Task[] tasks) 表示所有的task都執(zhí)行完畢后再去執(zhí)行后續(xù)的操作, task.WhenAny(Task[] tasks) 表示任一task執(zhí)行完畢后就開始執(zhí)行后續(xù)操作。看一個(gè)栗子:
- static void Main(string[] args)
- {
- Task task1 = new Task(() => {
- Thread.Sleep(500);
- Console.WriteLine("線程1執(zhí)行完畢!");
- });
- task1.Start();
- Task task2 = new Task(() => {
- Thread.Sleep(1000);
- Console.WriteLine("線程2執(zhí)行完畢!");
- });
- task2.Start();
- //task1,task2執(zhí)行完了后執(zhí)行后續(xù)操作
- Task.WhenAll(task1, task2).ContinueWith((t) => {
- Thread.Sleep(100);
- Console.WriteLine("執(zhí)行后續(xù)操作完畢!");
- });
- Console.WriteLine("主線程執(zhí)行完畢!");
- Console.ReadKey();
- }
執(zhí)行結(jié)果如下,我們看到WhenAll/WhenAny方法不會(huì)阻塞主線程,當(dāng)使用WhenAll方法時(shí)所有的task都執(zhí)行完畢才會(huì)執(zhí)行后續(xù)操作;如果把栗子中的WhenAll替換成WhenAny,則只要有一個(gè)線程執(zhí)行完畢就會(huì)開始執(zhí)行后續(xù)操作,這里不再演示。
上邊的栗子也可以通過 Task.Factory.ContinueWhenAll(Task[] tasks, Action continuationAction) 和 Task.Factory.ContinueWhenAny(Task[] tasks, Action continuationAction) 來實(shí)現(xiàn) ,修改上邊栗子代碼如下,執(zhí)行結(jié)果不變。
- static void Main(string[] args)
- {
- Task task1 = new Task(() => {
- Thread.Sleep(500);
- Console.WriteLine("線程1執(zhí)行完畢!");
- });
- task1.Start();
- Task task2 = new Task(() => {
- Thread.Sleep(1000);
- Console.WriteLine("線程2執(zhí)行完畢!");
- });
- task2.Start();
- //通過TaskFactroy實(shí)現(xiàn)
- Task.Factory.ContinueWhenAll(new Task[] { task1, task2 }, (t) =>
- {
- Thread.Sleep(100);
- Console.WriteLine("執(zhí)行后續(xù)操作");
- });
- Console.WriteLine("主線程執(zhí)行完畢!");
- Console.ReadKey();
- }
4 Task的任務(wù)取消(CancellationTokenSource)
1 Thread取消任務(wù)執(zhí)行
在Task前我們執(zhí)行任務(wù)采用的是Thread,Thread怎么取消任務(wù)呢?一般流程是:設(shè)置一個(gè)變量來控制任務(wù)是否停止,如設(shè)置一個(gè)變量isStop,然后線程輪詢查看isStop,如果isStop為true就停止,代碼如下:
- static void Main(string[] args)
- {
- bool isStop = false;
- int index = 0;
- //開啟一個(gè)線程執(zhí)行任務(wù)
- Thread th1 = new Thread(() =>
- {
- while (!isStop)
- {
- Thread.Sleep(1000);
- Console.WriteLine($"第{++index}次執(zhí)行,線程運(yùn)行中...");
- }
- });
- th1.Start();
- //五秒后取消任務(wù)執(zhí)行
- Thread.Sleep(5000);
- isStop = true;
- Console.ReadKey();
- }
2 Task取消任務(wù)執(zhí)行
Task中有一個(gè)專門的類 CancellationTokenSource 來取消任務(wù)執(zhí)行,還是使用上邊的例子,我們修改代碼如下,程序運(yùn)行的效果不變。
- static void Main(string[] args)
- {
- CancellationTokenSource source = new CancellationTokenSource();
- int index = 0;
- //開啟一個(gè)task執(zhí)行任務(wù)
- Task task1 = new Task(() =>
- {
- while (!source.IsCancellationRequested)
- {
- Thread.Sleep(1000);
- Console.WriteLine($"第{++index}次執(zhí)行,線程運(yùn)行中...");
- }
- });
- task1.Start();
- //五秒后取消任務(wù)執(zhí)行
- Thread.Sleep(5000);
- //source.Cancel()方法請(qǐng)求取消任務(wù),IsCancellationRequested會(huì)變成true
- source.Cancel();
- Console.ReadKey();
- }
CancellationTokenSource的功能不僅僅是取消任務(wù)執(zhí)行,我們可以使用 source.CancelAfter(5000) 實(shí)現(xiàn)5秒后自動(dòng)取消任務(wù),也可以通過 source.Token.Register(Action action) 注冊(cè)取消任務(wù)觸發(fā)的回調(diào)函數(shù),即任務(wù)被取消時(shí)注冊(cè)的action會(huì)被執(zhí)行。看一個(gè)栗子:
- static void Main(string[] args)
- {
- CancellationTokenSource source = new CancellationTokenSource();
- //注冊(cè)任務(wù)取消的事件
- source.Token.Register(() =>
- {
- Console.WriteLine("任務(wù)被取消后執(zhí)行xx操作!");
- });
- int index = 0;
- //開啟一個(gè)task執(zhí)行任務(wù)
- Task task1 = new Task(() =>
- {
- while (!source.IsCancellationRequested)
- {
- Thread.Sleep(1000);
- Console.WriteLine($"第{++index}次執(zhí)行,線程運(yùn)行中...");
- }
- });
- task1.Start();
- //延時(shí)取消,效果等同于Thread.Sleep(5000);source.Cancel();
- source.CancelAfter(5000);
- Console.ReadKey();
- }
執(zhí)行結(jié)果如下,第5次執(zhí)行在取消回調(diào)后打印,這是因?yàn)?,?zhí)行取消的時(shí)候第5次任務(wù)已經(jīng)通過了while()判斷,任務(wù)已經(jīng)執(zhí)行中了:
最后看上一篇跨線程的栗子,點(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);
- }
- });
- }
- }
運(yùn)行界面如下,賦值的task不會(huì)阻塞UI線程:
回到頂部
三、異步方法(async/await)
在C#5.0中出現(xiàn)的 async和await ,讓異步編程變得更簡(jiǎn)單。我們看一個(gè)獲取文件內(nèi)容的栗子:
- class Program
- {
- static void Main(string[] args)
- {
- string content = GetContentAsync(Environment.CurrentDirectory + @"/test.txt").Result;
- //調(diào)用同步方法
- //string content = GetContent(Environment.CurrentDirectory + @"/test.txt");
- Console.WriteLine(content);
- Console.ReadKey();
- }
- //異步讀取文件內(nèi)容
- async static Task<string> GetContentAsync(string filename)
- {
- FileStream fs = new FileStream(filename, FileMode.Open);
- var bytes = new byte[fs.Length];
- //ReadAync方法異步讀取內(nèi)容,不阻塞線程
- Console.WriteLine("開始讀取文件");
- int len = await fs.ReadAsync(bytes, 0, bytes.Length);
- string result = Encoding.UTF8.GetString(bytes);
- return result;
- }
- //同步讀取文件內(nèi)容
- static string GetContent(string filename)
- {
- FileStream fs = new FileStream(filename, FileMode.Open);
- var bytes = new byte[fs.Length];
- //Read方法同步讀取內(nèi)容,阻塞線程
- int len = fs.Read(bytes, 0, bytes.Length);
- string result = Encoding.UTF8.GetString(bytes);
- return result;
- }
- }
test.txt內(nèi)容是【hello world!】執(zhí)行結(jié)果為:
上邊的栗子也寫出了同步讀取的方式,將main函數(shù)中的注釋去掉即可同步讀取文件內(nèi)容。我們可以看到異步讀取代碼和同步讀取代碼基本一致。async/await讓異步編碼變得更簡(jiǎn)單,我們可以像寫同步代碼一樣去寫異步代碼。注意一個(gè)小問題:異步方法中方法簽名返回值為Task
異步方法簽名的返回值有以下三種:
① Task
② Task:如果調(diào)用方法不想通過異步方法獲取一個(gè)值,僅僅想追蹤異步方法的執(zhí)行狀態(tài),那么我們可以設(shè)置異步方法簽名的返回值為Task;
③ void:如果調(diào)用方法僅僅只是調(diào)用一下異步方法,不和異步方法做其他交互,我們可以設(shè)置異步方法簽名的返回值為void,這種形式也叫做“調(diào)用并忘記”。
小結(jié):到這里Task,async/await的簡(jiǎn)單使用已經(jīng)基本結(jié)束了,一些高級(jí)特性等到工作遇到了再去研究。通過上邊的介紹,我們知道async/await是基于Task的,而Task是對(duì)ThreadPool的封裝改進(jìn),主要是為了更有效的控制線程池中的線程(ThreadPool中的線程,我們很難通過代碼控制其執(zhí)行順序,任務(wù)延續(xù)和取消等等);ThreadPool基于Thread的,主要目的是減少Thread創(chuàng)建數(shù)量和管理Thread的成本。async/await Task是C#中更先進(jìn)的,也是微軟大力推廣的特性,我們?cè)陂_發(fā)中可以嘗試使用Task來替代Thread/ThreadPool,處理本地IO和網(wǎng)絡(luò)IO任務(wù)是盡量使用async/await來提高任務(wù)執(zhí)行效率。
本文轉(zhuǎn)載自微信公眾號(hào)「CSharp編程大全」,可以通過以下二維碼關(guān)注。轉(zhuǎn)載本文請(qǐng)聯(lián)系CSharp編程大全公眾號(hào)。