C#多線程和異步(二)——Task和async/await詳解

 


1、什麼是異步

  同步和異步主要用於修飾方法。當一個方法被調用時,調用者須要等待該方法執行完畢並返回才能繼續執行,咱們稱這個方法是同步方法;當一個方法被調用時當即返回,並獲取一個線程執行該方法內部的業務,調用者不用等待該方法執行完畢,咱們稱這個方法爲異步方法。編程

  異步的好處在於非阻塞(調用線程不會暫停執行去等待子線程完成),所以咱們把一些不須要當即使用結果、較耗時的任務設爲異步執行,能夠提升程序的運行效率。net4.0在ThreadPool的基礎上推出了Task類,微軟極力推薦使用Task來執行異步任務,如今C#類庫中的異步方法基本都用到了Task;net5.0推出了async/await,讓異步編程更爲方便。本篇主要介紹Task、async/await相關的內容,其餘異步操做的方式會在下一篇介紹。網絡

2、Task介紹

  Task是在ThreadPool的基礎上推出的,咱們簡單瞭解下ThreadPool。ThreadPool中有若干數量的線程,若是有任務須要處理時,會從線程池中獲取一個空閒的線程來執行任務,任務執行完畢後線程不會銷燬,而是被線程池回收以供後續任務使用。當線程池中全部的線程都在忙碌時,又有新任務要處理時,線程池纔會新建一個線程來處理該任務,若是線程數量達到設置的最大值,任務會排隊,等待其餘任務釋放線程後再執行。線程池能減小線程的建立,節省開銷,看一個ThreadPool的栗子吧多線程

複製代碼
static void Main(string[] args) { for (int i = 1; i <=10; i++) { //ThreadPool執行任務
                ThreadPool.QueueUserWorkItem(new WaitCallback((obj) => { Console.WriteLine($"第{obj}個執行任務"); }),i); } Console.ReadKey(); }
複製代碼

  上邊的代碼經過ThreadPool執行了10個任務,執行結果爲:異步

   ThreadPool相對於Thread來講能夠減小線程的建立,有效減少系統開銷;可是ThreadPool不能控制線程的執行順序,咱們也不能獲取線程池內線程取消/異常/完成的通知,即咱們不能有效監控和控制線程池中的線程。async

1 Task建立和運行

  咱們知道了ThreadPool的弊端:咱們不能控制線程池中線程的執行順序,也不能獲取線程池內線程取消/異常/完成的通知。net4.0在ThreadPool的基礎上推出了Task,Task擁有線程池的優勢,同時也解決了使用線程池不易控制的弊端。異步編程

首先看一下怎麼去建立並運行一個Task,Task的建立和執行方式有以下三種:函數

複製代碼
static void Main(string[] args) { //1.new方式實例化一個Task,須要經過Start方法啓動
            Task task = new Task(() => { Thread.Sleep(100); Console.WriteLine($"hello, task1的線程ID爲{Thread.CurrentThread.ManagedThreadId}"); }); task.Start(); //2.Task.Factory.StartNew(Action action)建立和啓動一個Task
            Task task2 = Task.Factory.StartNew(() => { Thread.Sleep(100); Console.WriteLine($"hello, task2的線程ID爲{ Thread.CurrentThread.ManagedThreadId}"); }); //3.Task.Run(Action action)將任務放在線程池隊列,返回並啓動一個Task
            Task task3 = Task.Run(() => { Thread.Sleep(100); Console.WriteLine($"hello, task3的線程ID爲{ Thread.CurrentThread.ManagedThreadId}"); }); Console.WriteLine("執行主線程!"); Console.ReadKey(); }
複製代碼

執行結果以下:post

  咱們看到先打印"執行主線程",而後再打印各個任務,說明了Task不會阻塞主線程。上邊的栗子Task都沒有返回值,咱們也能夠建立有返回值的Task<TResult>,用法和沒有返回值的基本一致,咱們簡單修改一下上邊的栗子,代碼以下:編碼

複製代碼
static void Main(string[] args) { ////1.new方式實例化一個Task,須要經過Start方法啓動
            Task<string> task = new Task<string>(() => { return $"hello, task1的ID爲{Thread.CurrentThread.ManagedThreadId}"; }); task.Start(); ////2.Task.Factory.StartNew(Func func)建立和啓動一個Task
           Task<string> task2 =Task.Factory.StartNew<string>(() => { return $"hello, task2的ID爲{ Thread.CurrentThread.ManagedThreadId}"; }); ////3.Task.Run(Func func)將任務放在線程池隊列,返回並啓動一個Task
           Task<string> task3= Task.Run<string>(() => { return $"hello, task3的ID爲{ Thread.CurrentThread.ManagedThreadId}"; }); Console.WriteLine("執行主線程!"); Console.WriteLine(task.Result); Console.WriteLine(task2.Result); Console.WriteLine(task3.Result); Console.ReadKey(); }
複製代碼

  注意task.Resut獲取結果時會阻塞線程,即若是task沒有執行完成,會等待task執行完成獲取到Result,而後再執行後邊的代碼,程序運行結果以下:   

  上邊的全部栗子中Task的執行都是異步的,不會阻塞主線程。有些場景下咱們想讓Task同步執行怎麼辦呢?Task提供了  task.RunSynchronously()用於同步執行Task任務,代碼以下:

複製代碼
static void Main(string[] args) { Task task = new Task(() => { Thread.Sleep(100); Console.WriteLine("執行Task結束!"); }); //同步執行,task會阻塞主線程
 task.RunSynchronously(); Console.WriteLine("執行主線程結束!"); Console.ReadKey(); }
複製代碼

執行結果以下:

 2 Task的阻塞方法(Wait/WaitAll/WaitAny)

1 Thread阻塞線程的方法

  使用Thread時,咱們知道用thread.Join()方法便可阻塞主線程。看一個例子:

複製代碼
static void Main(string[] args) { Thread th1 = new Thread(() => { Thread.Sleep(500); Console.WriteLine("線程1執行完畢!"); }); th1.Start(); Thread th2 = new Thread(() => { Thread.Sleep(1000); Console.WriteLine("線程2執行完畢!"); }); th2.Start(); //阻塞主線程
 th1.Join(); th2.Join(); Console.WriteLine("主線程執行完畢!"); Console.ReadKey(); }
複製代碼

  若是註釋掉兩個Join,執行結果是:先打印【主線程執行完畢】,而添加兩個Join方法後執行結果以下,實現了線程阻塞:

2 Task的Wait/WaitAny/WaitAll方法

   Thread的Join方法能夠阻塞調用線程,可是有一些弊端:①若是咱們要實現不少線程的阻塞時,每一個線程都要調用一次Join方法;②若是咱們想讓全部的線程執行完畢(或者任一線程執行完畢)時,當即解除阻塞,使用Join方法不容易實現。Task提供了  Wait/WaitAny/WaitAll  方法,能夠更方便地控制線程阻塞。

  task.Wait()  表示等待task執行完畢,功能相似於thead.Join();  Task.WaitAll(Task[] tasks)  表示只有全部的task都執行完成了再解除阻塞;  Task.WaitAny(Task[] tasks) 表示只要有一個task執行完畢就解除阻塞,看一個栗子: 

複製代碼
static void Main(string[] args) { Task task1 = new Task(() => { Thread.Sleep(500); Console.WriteLine("線程1執行完畢!"); }); task1.Start(); Task task2 = new Task(() => { Thread.Sleep(1000); Console.WriteLine("線程2執行完畢!"); }); task2.Start(); //阻塞主線程。task1,task2都執行完畢再執行主線程        //執行【task1.Wait();task2.Wait();】能夠實現相同功能
            Task.WaitAll(new Task[]{ task1,task2}); Console.WriteLine("主線程執行完畢!"); Console.ReadKey(); }
複製代碼

  執行結果以下:

  若是將栗子中的WaitAll換成WaitAny,那麼任一task執行完畢就會解除線程阻塞,執行結果是:先打印【線程1執行完畢】,而後打印【主線程執行完畢】,最後打印【線程2執行完畢】

 3 Task的延續操做(WhenAny/WhenAll/ContinueWith)

  上邊的Wait/WaitAny/WaitAll方法返回值爲void,這些方法單純的實現阻塞線程。咱們如今想讓全部task執行完畢(或者任一task執行完畢)後,開始執行後續操做,怎麼實現呢?這時就能夠用到WhenAny/WhenAll方法了,這些方法執行完成返回一個task實例。  task.WhenAll(Task[] tasks)  表示全部的task都執行完畢後再去執行後續的操做, task.WhenAny(Task[] tasks)  表示任一task執行完畢後就開始執行後續操做。看一個栗子:

複製代碼
static void Main(string[] args) { Task task1 = new Task(() => { Thread.Sleep(500); Console.WriteLine("線程1執行完畢!"); }); task1.Start(); Task task2 = new Task(() => { Thread.Sleep(1000); Console.WriteLine("線程2執行完畢!"); }); task2.Start(); //task1,task2執行完了後執行後續操做
            Task.WhenAll(task1, task2).ContinueWith((t) => { Thread.Sleep(100); Console.WriteLine("執行後續操做完畢!"); }); Console.WriteLine("主線程執行完畢!"); Console.ReadKey(); }
複製代碼

  執行結果以下,咱們看到WhenAll/WhenAny方法不會阻塞主線程,當使用WhenAll方法時全部的task都執行完畢纔會執行後續操做;若是把栗子中的WhenAll替換成WhenAny,則只要有一個線程執行完畢就會開始執行後續操做,這裏再也不演示。

   上邊的栗子也能夠經過 Task.Factory.ContinueWhenAll(Task[] tasks, Action continuationAction)  和 Task.Factory.ContinueWhenAny(Task[] tasks, Action continuationAction) 來實現 ,修改上邊栗子代碼以下,執行結果不變。

複製代碼
static void Main(string[] args) { Task task1 = new Task(() => { Thread.Sleep(500); Console.WriteLine("線程1執行完畢!"); }); task1.Start(); Task task2 = new Task(() => { Thread.Sleep(1000); Console.WriteLine("線程2執行完畢!"); }); task2.Start(); //經過TaskFactroy實現
            Task.Factory.ContinueWhenAll(new Task[] { task1, task2 }, (t) => { Thread.Sleep(100); Console.WriteLine("執行後續操做"); }); Console.WriteLine("主線程執行完畢!"); Console.ReadKey(); }
複製代碼

 4 Task的任務取消(CancellationTokenSource)

1 Thread取消任務執行

  在Task前咱們執行任務採用的是Thread,Thread怎麼取消任務呢?通常流程是:設置一個變量來控制任務是否中止,如設置一個變量isStop,而後線程輪詢查看isStop,若是isStop爲true就中止,代碼以下:

複製代碼
static void Main(string[] args) { bool isStop = false; int index = 0; //開啓一個線程執行任務
            Thread th1 = new Thread(() => { while (!isStop) { Thread.Sleep(1000); Console.WriteLine($"第{++index}次執行,線程運行中..."); } }); th1.Start(); //五秒後取消任務執行
            Thread.Sleep(5000); isStop = true; Console.ReadKey(); }
複製代碼

2 Task取消任務執行

  Task中有一個專門的類 CancellationTokenSource  來取消任務執行,仍是使用上邊的例子,咱們修改代碼以下,程序運行的效果不變。

複製代碼
static void Main(string[] args) { CancellationTokenSource source = new CancellationTokenSource(); int index = 0; //開啓一個task執行任務
            Task task1 = new Task(() => { while (!source.IsCancellationRequested) { Thread.Sleep(1000); Console.WriteLine($"第{++index}次執行,線程運行中..."); } }); task1.Start(); //五秒後取消任務執行
            Thread.Sleep(5000); //source.Cancel()方法請求取消任務,IsCancellationRequested會變成true
 source.Cancel(); Console.ReadKey(); }
複製代碼

   CancellationTokenSource的功能不只僅是取消任務執行,咱們可使用  source.CancelAfter(5000)  實現5秒後自動取消任務,也能夠經過  source.Token.Register(Action action)  註冊取消任務觸發的回調函數,即任務被取消時註冊的action會被執行。 看一個栗子:

複製代碼
static void Main(string[] args) { CancellationTokenSource source = new CancellationTokenSource(); //註冊任務取消的事件
            source.Token.Register(() => { Console.WriteLine("任務被取消後執行xx操做!"); }); int index = 0; //開啓一個task執行任務
            Task task1 = new Task(() => { while (!source.IsCancellationRequested) { Thread.Sleep(1000); Console.WriteLine($"第{++index}次執行,線程運行中..."); } }); task1.Start(); //延時取消,效果等同於Thread.Sleep(5000);source.Cancel();
            source.CancelAfter(5000); Console.ReadKey(); }
複製代碼

  執行結果以下,第5次執行在取消回調後打印,這是由於,執行取消的時候第5次任務已經經過了while()判斷,任務已經執行中了:

   最後看上一篇跨線程的栗子,點擊按鈕啓動一個任務,給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不會阻塞UI線程:

3、異步方法(async/await)

  在C#5.0中出現的 async和await ,讓異步編程變得更簡單。咱們看一個獲取文件內容的栗子:

複製代碼
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方法異步讀取內容,不阻塞線程
            Console.WriteLine("開始讀取文件"); 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方法同步讀取內容,阻塞線程
            int len =  fs.Read(bytes, 0, bytes.Length); string result = Encoding.UTF8.GetString(bytes); return result; } }
複製代碼

  test.txt內容是【hello world!】執行結果爲:

  上邊的栗子也寫出了同步讀取的方式,將main函數中的註釋去掉便可同步讀取文件內容。咱們能夠看到異步讀取代碼和同步讀取代碼基本一致。async/await讓異步編碼變得更簡單,咱們能夠像寫同步代碼同樣去寫異步代碼。注意一個小問題:異步方法中方法簽名返回值爲Task<T>,代碼中的返回值爲T。上邊栗子中GetContentAsync的簽名返回值爲Task<string>,而代碼中返回值爲string。牢記這一細節對咱們分析異步代碼頗有幫助。

  異步方法簽名的返回值有如下三種:

    ① Task<T>:若是調用方法想經過調用異步方法獲取一個T類型的返回值,那麼簽名必須爲Task<TResult>;

    ② Task:若是調用方法不想經過異步方法獲取一個值,僅僅想追蹤異步方法的執行狀態,那麼咱們能夠設置異步方法簽名的返回值爲Task;

    ③ void:若是調用方法僅僅只是調用一下異步方法,不和異步方法作其餘交互,咱們能夠設置異步方法簽名的返回值爲void,這種形式也叫作「調用並忘記」。

  小結:到這裏Task,async/await的簡單使用已經基本結束了,一些高級特性等到工做遇到了再去研究。經過上邊的介紹,咱們知道async/await是基於Task的,而Task是對ThreadPool的封裝改進,主要是爲了更有效的控制線程池中的線程(ThreadPool中的線程,咱們很難經過代碼控制其執行順序,任務延續和取消等等);ThreadPool基於Thread的,主要目的是減小Thread建立數量和管理Thread的成本。async/await Task是C#中更先進的,也是微軟大力推廣的特性,咱們在開發中能夠嘗試使用Task來替代Thread/ThreadPool,處理本地IO和網絡IO任務是儘可能使用async/await來提升任務執行效率。

【轉載:原文連接:http://www.javashuo.com/article/p-gfizbidr-md.html

相關文章
相關標籤/搜索