啓動一個程序,系統在內存中建立一個新進程,進程內部是系統建立的線程,線程能夠派生其餘線程,這就有了多線程。
進程內的多個線程共享進程的資源,系統爲處理器規劃的單元是線程。程序員
異步編程能夠實如今新線程裏面運行一部分代碼,或改變代碼的執行順序。編程
本章介紹瞭如下幾種異步編程方式,它們居可能是併發的而非並行。數組
async/await
BackgroundWorker
Task Parellel
System.Threading.Tasks
中的Parallel.For
和Parallel.ForEach
BeginInvoke/EndInvoke
System.Threading.Timer
啓動程序時,系統會在內存中建立一個新的進程。進程是構成運行程序的資源集合。包括虛地址空間、文件句柄和許多其餘程序運行所需的東西。服務器
在進程內部,系統建立了一個稱爲線程的內核(kernel)對象,它表明了真正執行的程序。(線程是「執行線程」的簡稱)一旦進程創建,系統會在Main方法的第一行語句處就開始線程的執行。網絡
關於線程,須要瞭解如下知識點多線程
本書目前爲止所展現的全部示例程序都只使用了一個線程,而且從程序的第一條語句按順序執行到最後一條。然而在不少狀況下,這種簡單的模型都會在性能或用戶體驗上致使難以接受的行爲。架構
例如,一個服務器程序可能會持續不斷地發起到其餘服務器的鏈接,並向它們請求數據,同時處理來自多個客戶端程序的請求。這種通訊任務每每耗費大量時間,在此期間程序只能等待網絡或互聯網上其餘計算機的響應。這嚴重削弱了性能。程序不該該浪費等待響應的時間,而應該更加高效,在等待的同時執行其餘任務,回覆到達後再繼續執行第一個任務。併發
本章咱們將學習異步編程。在異步程序中,程序代碼不須要按照編寫的順序嚴格執行。有時須要在一個新的線程中運行一部分代碼,有時無需建立新的線程,但爲了更好地利用單個線程的能力,須要改變代碼的執行順序。框架
咱們先來看看C#5.0引入的一個用來構建異步方法的新特性——async/await
。接下來學習一些可實現其餘形式的異步編程的特性,這些特性是.NET框架的一部分,但沒有嵌入C#語言。相關主題包括BackgroundWorker
類和.NET任務並行庫。二者均經過新建線程來實現異步。本章最後咱們會看看編寫異步程序的其餘方式。異步
爲了演示和比較,咱們先來看一個不使用異步的示例。而後再看一個實現相似功能的異步程序。
在下面的代碼示例中,MyDownloadString類的方法DoRun執行如下任務。
System.Diagnostics
命名空間)的一個實例並啓動。該Stopwatch計時器用來測量代碼中不一樣任務的執行時間using System; using System.Net; using System.Diagnostics; class MyDownloadString { Stopwatch sw = new Stopwatch(); public void DoRun() { const int LargeNumber = 6000000; sw.Start(); int t1 = CountCharacters(1, "http://www.microsoft.com"); int t2 = CountCharacters(2, "http://www.illustratedcsharp.com"); CountToALargeNumber(1, LargeNumber); CountToALargeNumber(2, LargeNumber); CountToALargeNumber(3, LargeNumber); CountToALargeNumber(4, LargeNumber); Console.WriteLine("Chars in http://www.microsoft.coin :{0}", t1); Console.WriteLine("Chars in http://www.illustratedcsharp.com: {0}", t2); } private int CountCharacters(int id, string uriString) { WebClient wc1 = new WebClient(); Console.WriteLine("Starting call {0} : {1, 4:N} ms", id, sw.Elapsed.TotalMilliseconds); string result = wc1.DownloadString(new Uri(uriString)); Console.WriteLine(" Call {0} completed: {1, 4:N} ms", id, sw.Elapsed.TotalMilliseconds); return result.Length; } private void CountToALargeNumber(int id, int value) { for (long i = 0; i < value; i++) ; Console.WriteLine(" End counting {0} : {1,4:N} ms", id, sw.Elapsed.TotalMilliseconds); } } class Program { static void Main() { MyDownloadString ds = new MyDownloadString(); ds.DoRun(); Console.ReadKey(); } }
輸出:
Starting call 1 : 0.39 ms Call 1 completed: 131.95 ms Starting call 2 : 132.04 ms Call 2 completed: 655.97 ms End counting 1 : 670.80 ms End counting 2 : 685.57 ms End counting 3 : 700.00 ms End counting 4 : 714.46 ms Chars in http://www.microsoft.coin :1020 Chars in http://www.illustratedcsharp.com: 210
下圖總結了輸出結果,展現了不一樣任務開始和結束的時間。如圖所示,Call1和Call2佔用了大部分時間。但無論哪次調用,絕大部分時間都浪費在等待網站的響應上。
若是咱們能初始化兩個CountCharacter調用,無需等待結果,而是直接執行4個CountToALargeNumber調用,而後在兩個CountCharacter方法調用結束時再獲取結果,就能夠顯著地提高性能。
C#最新的async/await
特性就容許咱們這麼作。能夠重寫代碼以運用該特性,以下所示。稍後我會深刻剖析這個特性,如今先來看看本示例須要注意的幾個方面。
Task<int>
類型的佔位符對象,表示它計劃進行的工做。這個佔位符最終將「返回」一個intTask<int>
對象Tasks
中獲取結果。若是尚未結果,將阻塞並等待using System; using System.Diagnostics; using System.Net; using System.Threading.Tasks; class MyDownloadString { Stopwatch sw = new Stopwatch(); public void DoRun() { const int LargeNumber = 6000000; sw.Start(); Task<int> t1 = CountCharactersAsync(1, "http://www.microsoft.com"); Task<int> t2 = CountCharactersAsync(2, "http://www.illustratedcsharp.com"); CountToALargeNumber(1, LargeNumber); CountToALargeNumber(2, LargeNumber); CountToALargeNumber(3, LargeNumber); CountToALargeNumber(4, LargeNumber); Console.WriteLine("Chars in http://www.microsoft.coin :{0}", t1.Result); Console.WriteLine("Chars in http://www.illustratedcsharp.com: {0}", t2.Result); } private async Task<int> CountCharactersAsync(int id, string site) { WebClient wc = new WebClient(); Console.WriteLine("Starting call {0} : {1, 4:N} ms", id, sw.Elapsed.TotalMilliseconds); string result = await wc.DownloadStringTaskAsync(new Uri(site)); Console.WriteLine(" Call {0} completed : {1, 4:N} ms", id, sw.Elapsed.TotalMilliseconds); return result.Length; } private void CountToALargeNumber(int id, int value) { for (long i = 0; i < value; i++) ; Console.WriteLine(" End counting {0} : {1,4:N} ms", id, sw.Elapsed.TotalMilliseconds); } } class Program { static void Main() { MyDownloadString ds = new MyDownloadString(); ds.DoRun(); Console.ReadKey(); } }
輸出:
Starting call 1 : 1.33 ms Starting call 2 : 66.50 ms End counting 1 : 83.81 ms End counting 2 : 124.33 ms Call 1 completed : 124.35 ms End counting 3 : 138.55 ms End counting 4 : 152.52 ms Chars in http://www.microsoft.coin :1020 Call 2 completed : 623.79 ms Chars in http://www.illustratedcsharp.com: 210
下圖總結了輸出結果,展現了修改後的程序的時間軸。新版程序比舊版快了32%。這是因爲 CountToALargeNumber 的4次調用是在 CountCharactersAsync 方法調用等待網站響應的時候進行的。全部這些工做都是在主線程中完成的,咱們沒有建立任何額外的線程!
咱們已經看到了一個異步方法的示例,如今來討論其定義和細節。
若是一個程序調用某個方法,等待其執行全部處理後才繼續執行,咱們就稱這樣的方法是同步的。這是默認形式,在本章以前你所看到的都是這種形式。
相反,異步的方法在處理完成以前就返回到調用方法。C#的async/await
特性能夠建立並使用異步方法。該特性由三個部分組成,以下所示。
Class Program { static void Main() { ... //調用方法 Task<int> value=DoAsyncStuff.CalculateSumAsync(5,6); ... } } static class DoAsyncStuff { //異步方法 public static async Task<int> CalculateSumAsync(int i1,int i2) { //await表達式 int sum=await TaskEx.Run(()=>GetSum(i1,i2)); return sum; } ... }
如上節所述,異步方法在完成其工做以前即返回到調用方法,而後在調用方法繼續執行的時候完成其工做。
在語法上,異步方法具備以下特色,以下圖。
async
方法修飾符await
表達式,表示能夠異步完成的任務。Task
)和第三種(Task<T>
)的返回對象表示將在將來完成的工做,調用方法和異步方法能夠繼續執行
void
Task
Task<T>
關鍵字 返回類型 ↓ ↓ async Task<int> CountCharactersAsync(int id,string site) { WebClient wc = new WebClient(); Console.WriteLine( "Starting call {0} : {1, 4:N} ms",id, sw.Elapsed.TotalMilliseconds); // await表達式 string result = await wc.DownloadStringTaskAsync( new Uri(site)); Console.WriteLine( " Call {0} completed: {1, 4:N} ms",id, sw.Elapsed.TotalMilliseconds); // 返回語句 return result.Length; }
上例闡明瞭一個異步方法的組成部分,如今咱們能夠詳細介紹了。
第一項是async
關鍵字。
async
關鍵字,且必須出如今返回類型以前await
表達式。也就是說,它自己並不能建立任何異步操做。async
關鍵字是一個上下文關鍵字,也就是說除了做爲方法修飾符(或Lambda表達式修飾符、匿名方法修飾符)以外,async
還可用做標識符返回類型必須是如下三種類型之一。注意,其中兩種都涉及Task類。我在指明類的時候,將使用大寫形式(類名)和語法字體來區分。在表示一系列須要完成的工做時,將使用小寫字母和通常字體。
Task<T>
:若是調用方法要從調用中獲取一個T類型的值,異步方法的返回類型就必須是Task<T>
。調用方法將經過讀取Task的Result屬性來獲取這個T類型的值。下面的代碼來自一個調用方法,闡明瞭這一點:Task<int> value = DoStuff.CalculateSumAsync(5,6); ... Console,WriteLine( "Value: {0}", value.Result);
Task
:若是調用方法不須要從異步方法中返回某個值,但須要檢査異步方法的狀態,那麼異步方法能夠返回一個Task類型的對象。這時,即便異步方法中出現了 return語句,也不會返回任何東西。下面的代碼一樣來自調用方法:Task someTask = DoStuff.CalculateSumAsync(5,6); ... someTask.Wait();
void
:若是調用方法僅僅想執行異步方法,而不須要與它作任何進一步的交互時[這稱爲調用並忘記(fire and forget)],異步方法能夠返回void類型。這時,與上一種狀況相似,即便異步方法中包含任何return語句,也不會返回任何東西注意上例中異步方法的返回類型爲Task<int>
。但方法體中不包含任何返回Task<int>
類型對象的return語句。相反,方法最後的return語句返回了一個int類型(result.Length)的值。咱們先將這一發現總結以下,稍後再詳細解釋。
Task<T>
類型的異步方法其返回值必須爲T類型或能夠隱式轉換爲T的類型下面闡明瞭調用方法和異步方法在用這三種返回類型進行交互時所需的體系結構。
使用返回Task<int>
對象的異步方法
using System; using System.Threading.Tasks; class Program { static void Main() { Task<int> value=DoAsyncStuff.CalculateSumAsync(5,6); //處理其餘事情 Console.WriteLine("Value: {0}",value.Result); } } static class DoAsyncStuff { public static async Task<int> CalculateSumAsync(int i1,int i2) { int sum=await Task.Run(()=>GetSum(i1,i2)); return sum; } private static int GetSum(int i1,int i2) { return i1+i2; } }
使用返回Task
對象的異步方法
using System; using System.Threading.Tasks; class Program { static void Main() { Task someTask=DoAsyncStuff.CalculateSumAsync(5,6); //處理其餘事情 someTask.Wait(); Console.WriteLine("Async stuff is done"); } } static class DoAsyncStuff { public static async Task CalculateSumAsync(int i1,int i2) { int value=await Task.Run(()=>GetSum(i1,i2)); Console.WriteLine("Value: {0}",value); } private static int GetSum(int i1,int i2) { return i1+i2; } }
輸出:
Value: 11 Async stuff is done
下例中使用Thread.Sleep
方法來暫停當前線程,因此異步方法完成時,它尚未完成。
使用「調用並忘記」的異步方法
using System; using System.Threading; using System.Threading.Tasks; class Program { static void Main() { DoAsyncStuff.CalculateSumAsync(5,6); //處理其餘事情 Thread.Sleep(200); Console.WriteLine("Program Exiting"); } } static class DoAsyncStuff { public static async void CalculateSumAsync(int i1,int i2) { int value=await Task.Run(()=>GetSum(i1,i2)); Console.WriteLine("Value: {0}",value); } private static int GetSum(int i1,int i2) { return i1+i2; } }
輸出:
Value: 11 Program Exiting
異步方法的結構包含三個不一樣的區域,以下圖所示。我將在下節詳細介紹await表達式,不過在本節你將對其位置和做用有個大體瞭解。這三個區域以下:
下圖闡明瞭一個異步方法的控制流。它從第一個await表達式以前的代碼開始,正常執行 (同步地)直到碰見第一個await。這一區域實際上在第一個await表達式處結束,此時await任務尚未完成(大多數狀況下如此)。當await任務完成時,方法將繼續同步執行。若是還有其餘await,就重複上述過程。
當達到await表達式時,異步方法將控制返回到調用方法。若是方法的返回類型爲Task
或Task<T>
類型,將建立一個Task
對象,表示需異步完成的任務和後續,而後將該Task
返回到調用方法。
目前有兩個控制流:異步方法內的和調用方法內的。異步方法內的代碼完成如下工做。
void
,控制流將退出Task
,後續部分設置Task
的屬性並退出。若是返回類型爲Task<T>
,後續部分還將設置Task
對象的 Result 屬性同時,調用方法中的代碼將繼續其進程,從異步方法獲取Task
對象。當須要其實際值時,就引用Task
對象的 Result 屬性。屆時,若是異步方法設置了該屬性,調用方法就能得到該值並繼續。不然,將暫停並等待該屬性被設置,而後再繼續執行。
不少人可能不解的一點是同步方法第一次遇到await時所返回對象的類型。這個返回類型就是同步方法頭中的返回類型,它與await表達式的返回值類型一點關係也沒有。
例以下面的代碼,await表達式返回一個string。但在方法的執行過程當中,當到達await表達式時,異步方法返回到調用方法的是一個Task<int>
對象,這正是該方法的返回類型。
private async Task<int> CountCharactersAsync(string site) { WebClient wc=new WebClient(); string result=await wc.DownloadStringTaskAsync(new Uri(site)); return result.Length; }
另外一個可能讓人迷惑的地方是,異步方法的return語句「返回」一個結果或到達異步方法末尾時,它並無真正地返回某個值——它只是退出了。
await表達式指定了一個異步執行的任務。其語法以下所示,由await關鍵字和一個空閒對象 (稱爲任務)組成。這個任務多是一個Task
類型的對象,也可能不是。默認狀況下,這個任務在當前線程異步運行。
await task
一個空閒對象便是一個awaitable
類型的實例。awaitable
類型是指包含GetAwaiter
方法的類型,該方法沒有參數,返回一個稱爲awaiter
類型的對象。awaiter
類型包含如下成員:
bool IsCompleted{get;}
void OnCompleted(Action);
它還包含如下成員之一:
void GetResult();
T GetResult();//T爲任意類型
然而實除上,你並不須要構建本身的awaitable
。相反,你應該使用Task
類,它是awaitable
類型。對於awaitable
,大多數程序員所須要的就是Task
了。
在.NET4.5中,微軟發佈了大量新的和修訂的異步方法(在BCL中),它們可返回Task<T>
類型的對象。將這些放到你的await表達式中,它們將在當前線程中異步執行。
在以前的不少示例中,咱們都使用了WebClient.DownloadStringTaskAsync
方法,它也是這些異步方法中一個。如下代碼闡明瞭其用法:
Uri site = new Uri("http://www.illustratedcsharp.com"); WebClient wc = new WebClient(); string result = await wc.DownloadStringTaskAsync(site);
儘管目前BCL中存在不少返回Task<T>
類型對象的方法,你仍然可能須要編寫本身的方法, 做爲await表達式的任務。最簡單的方式是在你的方法中使用Task.Run
方法來建立一個Task
。關於Task.Run
,有一點很是重要,即它是在不一樣的線程上運行你的方法。
Task.Run
的一個簽名以下,以Func<TReturn>
委託(Delegate)爲參數。如第19章所述,Func<TReturn>
是一個預約義的委託,它不包含任何參數,返回值的類型爲TReturn
:
Task Run(Func<TReturn> func)
所以,要將你的方法傳遞給Task.Run
方法,須要基於該方法建立一個委託。下面的代碼展現了三種實現方式。其中,Get10與Func<int>
委託兼容,由於它沒有參數而且返回int。
Func<int>
委託。而後在下一行將該委託用於Task.Run方法Func<int>
委託Func<int>
委託兼容的Lambda表達式。該Lambda表達式將隱式轉換爲該委託class MyClass { public int Get10() { return 10; } public async Task DoWorkAsync() { // 單首創建 Func<TReturn> 委託 Func<int> ten=new Func<int>(Get10); int a=await Task.Run(ten); // 參數列表中建立 Func<TReturn> 委託 int b=await Task.Run(new Func<int>(Get10)); // 隱式轉換爲 Func<TReturn> 委託的 Lambda表達式 int c=await Task.Run(()=>{return 10;}); Console.WriteLine("{0} {1} {2}",a,b,c); } } class Program { static void Main() { Task t=(new MyClass()).DoWorkAsync(); t.Wait(); } }
輸出:
10 10 10
在上面的示例代碼中,咱們使用的Task.Run的簽名以Func<TResult>
爲參數。該方法共有8個重載,以下表所示。
下表展現了可能用到的4個委託類型的簽名。
下面的代碼展現了4個await語句,使用Task.Run
方法來運行4種不一樣的委託類型所表示的方法:
static class MyClass { public static async Task DoWorkAsync() { Action ↓ await Task.Run(() => Console.WriteLine(5.ToString())); TResult Func() ↓ Console.WriteLine((await Task.Run(() => 6)).ToString()); Task Func() ↓ await Task.Run(() => Task.Run(() => Console.WriteLine(7.ToString()))); Task<TResult> Func() ↓ int value = await Task.Run(() => Task.Run(() => 8)); Console.WriteLine(value.ToString()); } } class Program { static void Main() { Task t = MyClass.DoWorkAsync(); t.Wait(); Console.WriteLine("Press Enter key to exit"); Console.Read(); } }
輸出:
5 6 7 8 Press Enter key to exit
在能使用任何其餘表達式的地方,均可以使用await表達式(只要位於異步方法內)。在上面的代碼中,4個await表達式用在了3個不一樣的位置。
假設咱們的某個方法不符合這4種委託形式。例如,假設有一個GetSum
方法以兩個int值做爲輸入,並返回這兩個值的和。這與上述4個可接受的委託都不兼容。要解決這個問題,能夠用可接受的Func
委託的形式建立一個Lambda函數,其惟一的行爲就是運行GetSum
方法,以下面的代碼所示:
int value = await Task.Run(()=> GetSum(5,6));
Lambda函數()=>GetSum(5,6)
知足Func<TResult>
委託,由於它沒有參數,且返回單一的值。
下面的代碼展現了完整的示例:
static class MyClass { private static int GetSum(int i1, int i2) { return i1+i2; } public static async Task DoWorkAsync() { int value=await Task.Run(()=>GetSum(5,6)); Console.WriteLine(value.ToString()); } } class Program { static void Main() { Task t = MyClass.DoWorkAsync(); t.Wait(); Console.WriteLine("Press Enter key to exit"); Console.Read(); } }
輸出:
11 Press Enter key to exit
一些.NET異步方法容許你請求終止執行。你一樣也能夠在本身的異步方法中加入這個特性。
System.Threading.Tasks
命名空間中有兩個類是爲此目的而設計的:CancellationToken
和CancellationTokenSource
。
CancellationToken
對象包含一個任務是否應被取消的信息CancellationToken
對象的任務須要按期檢查其令牌(token)狀態。若是CancellationToken
對象的IsCancellationRequested
屬性爲true
,任務需中止其操做並返回CancellationToken
是不可逆的,而且只能使用一次。也就是說,一旦IsCancellationRequested
屬性被設置爲true
,就不能更改了CancellationTokenSource
對象建立可分配給不一樣任務的CancellationToken
對象。任何持有CancellationTokenSource
的對象均可以調用其Cancel
方法,這會將CancellationToken
的IsCancellationRequested
屬性設置爲true
下面的代碼展現瞭如何使用CancellationTokenSource
和CancellationToken
來實現取消操做。注意,該過程是協同的。即調用CancellationTokenSource
的Cancel
時,它自己並不會執行取消操做。而是會將CancellationToken
的IsCancellationRequested
屬件設置爲true
。包含CancellationToken
的代碼負責檢查該屬性,並判斷是否須要中止執行並返回。
下面的代碼展現瞭如何使用這兩個取消類。以下所示代碼並無取消異步方法,而是在Main方法中間有兩行被註釋的代碼,它們觸發了取消行爲。
using System; using System.Threading; using System.Threading.Tasks; class Program { static void Main() { CancellationTokenSource cts = new CancellationTokenSource(); CancellationToken token = cts.Token; MyClass mc = new MyClass(); Task t = mc.RunAsync(token); //Thread.Sleep(3000);//等待3秒 //cts.Cancel(); //取消操做 t.Wait(); Console.WriteLine("Was Cancelled: {0}", token.IsCancellationRequested); Console.ReadKey(); } } class MyClass { public async Task RunAsync(CancellationToken ct) { if (ct.IsCancellationRequested) return; await Task.Run(() => CycleMethod(ct), ct); } // CycleMethod完全執行完須要5s void CycleMethod(CancellationToken ct) { Console.WriteLine("Starting CycleMethod"); const int max = 5; for (int i = 0; i < max; i++) { if (ct.IsCancellationRequested) // 監控CancellationToken return; Thread.Sleep(1000); Console.WriteLine(" {0} of {1} iterations completed", i + 1, max); } } }
第一次運行時保留註釋的代碼,不會取消任務,產生的結果以下:
Starting CycleMethod 1 of 5 iterations completed 2 of 5 iterations completed 3 of 5 iterations completed 4 of 5 iterations completed 5 of 5 iterations completed Was Cancelled: False
若是取消Main方法中對Thread.Sleep
和Cancel
語句的屏蔽,任務將在3秒後取消,產生的結果以下:
Starting CycleMethod 1 of 5 iterations completed 2 of 5 iterations completed 3 of 5 iterations completed Was Cancelled: True
能夠像使用其餘表達式那樣,將await表達式放在try語句內,try…catch…finally結構將按你指望的那樣工做。
下面的代碼展現了一個示例,其中await表達式中的任務會拋出一個異常。await表達式位於try塊中,將按普通的方式處理異常。
class Program { static void Main(string[] args) { Task t = BadAsync(); t.Wait(); Console.WriteLine("Task Status : {0}", t.Status); Console.WriteLine("Task IsFaulted: {0}", t.IsFaulted); } static async Task BadAsync() { try { await Task.Run(() => { throw new Exception(); }); } catch { Console.WriteLine("Exception in BadAsync"); } } }
輸出:
Exception in BadAsync Task Status : RanToCompletion Task IsFaulted: False
注意,儘管Task拋出了一個Exception,在Main的最後,Task的狀態仍然爲RanToCompletion。這會讓人感到很意外,由於異步方法拋出了異常。
緣由是如下兩個條件成立:(1)Task沒有被取消,(2)沒有未處理的異常。相似地,IsFaulted
屬性爲False
,由於沒有未處理的異常。
調用方法能夠調用任意多個異步方法並接收它們返回的Task對象。而後你的代碼會繼續執行其餘任務,但在某個點上可能會須要等待某個特殊Task對象完成,而後再繼續。爲此,Task
類提供了一個實例方法Wait
,能夠在Task對象上調用該方法。
下面的示例展現了其用法。在代碼中,調用方法DoRun
調用異步方法CountCharactersAsync
並接收其返回的Task<int>
。而後調用Task
實例的Wait
方法,等待任務Task結束。等結束時再顯示結果信息。
static class MyDownloadString { public static void DoRun() { Task<int> t = CountCharactersAsync("https://www.zhihu.com/"); //t.Wait(); Console.WriteLine("The task is executing."); Console.WriteLine("The task has finished, returning value {0}.", t.Result); } private static async Task<int> CountCharactersAsync(string site) { string result = await new WebClient().DownloadStringTaskAsync(new Uri(site)); return result.Length; } } class Program { static void Main() { MyDownloadString.DoRun(); Console.ReadKey(); } }
輸出:
The task is executing. The task has finished, returning value 8328.
屏蔽t.Wait();
時,先輸出第一句,Task<int> t
執行完成後輸出第二句;不屏蔽t.Wait();
時,Task<int> t
執行完成後同時輸出這兩句。
Wait
方法用於單一Task對象。而你也能夠等待一組Task對象。對於一組Task,能夠等待全部任務都結束,也能夠等待某一個任務結束。實現這兩個功能的是Task
類中的兩個靜態方法:
WaitAll
WaitAny
這兩個方法是同步方法且沒有返回值。它們中止,直到條件知足後再繼續執行。
咱們來看一個簡單的程序,它包含一個DoRun
方法,兩次調用一個異步方法並獲取其返回的兩個Task<int>
對象。而後,方法繼續執行,檢査任務是否完成並打印。
以下所示的程序並無使用等待方法,而是在DoRun
方法中間註釋的部分包含等待的代碼,咱們將在稍後用它來與如今的版本進行比較。
class MyDownloadString { Stopwatch sw = new Stopwatch(); public void DoRun() { sw.Start(); Task<int> t1 = CountCharactersAsync( 1, "http://www.microsoft.com"); Task<int> t2 = CountCharactersAsync( 2, "http://www.illustratedcsharp.com" ); //Task<int>[] tasks = new Task<int>[]{ t1, t2 }; //Task.WaitAll( tasks ); //Task.WaitAny( tasks ); Console.WriteLine( "Task 1: {0}Finished", t1.IsCompleted ? "" : "Not "); Console.WriteLine( "Task 2: {0}Finished", t2.IsCompleted ? "" : "Not "); Console.Read(); } private async Task<int> CountCharactersAsync( int id, string site ) { WebClient wc = new WebClient(); string result = await wc.DownloadStringTaskAsync( new Uri( site )); Console.WriteLine(" Call {0} completed: {1} ms",id, sw.Elapsed.TotalMilliseconds ); return result.Length; } } class Program { static void Main() { MyDownloadString ds = new MyDownloadString(); ds.DoRun(); } }
代碼產生的結果以下。注意,在檢査這兩個Task
的IsCompleted
方法時,沒有一個是完成的。
Task 1: Not Finished Task 2: Not Finished Call 1 completed: 127.3862 ms Call 2 completed: 647.7455 ms
若是咱們取消DoRun
中間那兩行代碼中第一行的註釋(以下面的三行代碼所示),方法將建立一個包含這兩個任務的數組,並將這個數組傳遞給WaitAll
方法。這時代碼會中止並等待任務所有完成,而後繼續執行。
Task<int>[] tasks = new Task<int>[] {t1,t2}; Task.WaitAll( tasks ); //Task.WaitAny( tasks );
此時運行代碼,其結果以下:
Call 1 completed: 100.8518 ms Call 2 completed: 551.1589 ms Task 1: Finished Task 2: Finished
若是咱們再次修改代碼,註釋掉WaitAll
方法調用,取消WaitAny
方法調用的註釋,代碼將以下所示:
Task<int>[] tasks = new Task<int>[] {t1,t2}; //Task.WaitAll( tasks ); Task.WaitAny( tasks );
這時,WaitAny
調用將終止並等待至少一個任務完成。運行代碼的結果以下:
Call 1 completed: 158.7846 ms Task 1: Finished Task 2: Not Finished Call 2 completed: 610.8676 ms
WaitAll
和WaitAny
分別還包含4個重載,除了任務完成以外,還容許其餘繼續執行的方式,如設置超時時間或使用CancellationToken
來強制執行處理的後續部分。下表展現了這些重載方法。
上節學習瞭如何同步地等待Task完成。但有時在異步方法中,你會但願用await表達式來等待Task。這時異步方法會返回到調用方法,但該異步方法會等待一個或全部任務完成。能夠經過Task.WhenAll
和Task.WhenAny
方法來實現。這兩個方法稱爲組合子(combinator)。
下面的代碼展現了一個使用Task.WhenAll
方法的示例。它異步地等待全部與之相關的Task完成,不會佔用主線程的時間。注意,await表達式的任務就是調用Task.WhenAll
。
using System; using System.Collections.Generic; using System.Net; using System.Threading.Tasks; class MyDownloadString { public void DoRun() { Task<int> t = CountCharactersAsync( "http://www.microsoft.com", "http://www.illustratedcsharp.com"); Console.WriteLine( "DoRun: Task {0}Finished", t.IsCompleted ? "": "Not " ); Console.WriteLine( "DoRun: Result = {0}", t.Result ); } private async Task<int> CountCharactersAsync(string sitel, string site2) { WebClient wcl = new WebClient(); WebClient wc2 = new WebClient(); Task<string> t1 = wcl.DownloadStringTaskAsync( new Uri( sitel )); Task<string> t2 = wc2.DownloadStringTaskAsync( new Uri( site2 )); List<Task<string>> tasks = new List<Task<string>>(); tasks.Add( t1 ); tasks.Add( t2 ); await Task.WhenAll( tasks ); Console.WriteLine(" CCA: T1 {0}Finished", t1.IsCompleted ? "" : "Not "); Console.WriteLine(" CCA: T2 {0}Finished", t2.IsCompleted ? "" : "Not "); return t1.IsCompleted ? t1.Result.Length : t2.Result.Length; } } class Program { static void Main() { var ds=new MyDownloadString(); ds.DoRun(); } }
輸出:
DoRun: Task Not Finished CCA: T1 Finished CCA: T2 Finished DoRun: Result = 1020
Task.WhenAny
組合子會異步地等待與之相關的某個Task完成。若是將上面的await表達式由調用Task.WhenAll
改成調用Task.WhenAny
,並返回到程序,將產生如下輸出結果:
DoRun: Task Not Finished CCA: T1 Finished CCA: T2 Not Finished DoRun: Result = 1020
Task.Delay
方法建立一個Task對象,該對象將暫停其在線程中的處理,並在必定時間以後完成。然而與Thread.Sleep
阻塞線程不一樣的是,Task.Delay
不會阻塞線程,線程能夠繼續處理其餘工做。
下面的代碼展現瞭如何使用Task.Delay
方法:
class Simple { Stopwatch sw = new Stopwatch(); public void DoRun() { Console.WriteLine( "Caller: Before call"); ShowDelayAsync(); Console.WriteLine( "Caller: After call"); } private async void ShowDelayAsync() { sw.Start(); Console.WriteLine( " Before Delay: {0}", sw.ElapsedMilliseconds ); await Task.Delay( 1000 ); Console.WriteLine( " After Delay : {0}", sw.ElapsedMilliseconds ); } } class Program { static void Main() { var ds = new Simple (); ds.DoRun(); Console.Read(); } }
輸出:
Caller: Before call Before Delay: 0 Caller: After call After Delay : 1013
Delay
方法包含4個重載,能夠以不一樣方式來指定時間週期,同時還容許使用CancellationToken
對象。下表展現了該方法的4個重載。
儘管本章目前的全部代碼均爲控制檯應用程序,但實際上異步方法在GUI程序中尤其有用。
緣由是GUI程序在設計上就要求全部的顯示變化都必須在主GUI線程中完成,如點擊按鈕、展現標籤、移動窗體等。Windows程序是經過消息來實現這一點的,消息被放入由消息泵管理的消息隊列中。
消息泵從隊列中取出一條消息,並調用它的處理程序(handler)代碼。當處理程序代碼完成時,消息泵獲取下一條消息並循環這個過程。
因爲這種架構,處理程序代碼就必須快捷,這樣纔不至於掛起並阻礙其餘GUI行爲的處理。若是某個消息的處理程序代碼耗時過長,消息隊列中的消息會產生積壓。程序將失去響應,由於在那個長時間運行的處理程序完成以前,沒法處理任何消息。
下圖展現了一個WPF程序中兩個版本的窗體。窗體由狀態標籤及其下方的按鈕組成。開發者的目的是,程序用戶點擊按鈕,按鈕的處理程序代碼執行如下操做:
右圖的截屏展現了開發者但願在按鈕按下的4秒以內窗體的樣子。然而事實並不是如此。當開發者點擊按鈕後,什麼都沒有發生。並且若是在點擊按鈕後移動窗體,會發現它已經凍結,不會移動——直到4秒以後,窗體才忽然出如今新位置。
注意 WPF是微軟替代Windows Form的GUI編程框架。要了解更多關於WPF編程的知識,請參閱筆者的 Illustrated WPF(Apress,2009)一書。
要使用Visual Studio 2012建立這個名爲MessagePump的WPF程序,步驟以下:
1.選擇File→New→Project菜單項,彈出New Project窗口
2.在窗口左側的面板內,展開Installed Templates(若是沒有展開的話)
3.在C#類別中點擊Windows條目,將在中間面板中彈出已安裝的Windows程序模板
4.點擊WPF Application,在窗口下方的Name文本框中輸人MessagePump。在其下方選擇一個位置,並點擊OK按鈕
5.將MainWindow.xaml中的XAML標記修改成下面的代碼,在窗體中建立狀態標籤和按鈕。
<Window x:Class="MessagePump.MainWindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Title="Pump" Height="120" Width="200"> <StackPanel> <Label Name="lblStatus" Margin="10,5,10,0" >Not Doing Anything</Label> <Button Name="btnDoStuff" Content="Do Stuff" HorizontalAlignment="Left" Margin="10,5" Padding="5,2" Click="btnDoStuff_Click"/> </StackPanel> </Window>
6.將代碼隱藏文件MainWindow.xaml.cs修改成以下C#代碼。
using System.Threading; using System.Threading.Tasks; using System.Windows; namespace MessagePump { public partial class MainWindow : Window { public MainWindow() { InitializeComponent(); } private void btnDoStuff_Click( object sender, RoutedEventArgs e ) { btnDoStuff.IsEnabled = false; lblStatus.Content = "Doing Stuff"; Thread.Sleep( 4000 ); lblStatus.Content = "Not Doing Anything"; btnDoStuff.IsEnabled = true; } } }
運行程序,你會發現其行爲與以前的描述徹底一致,即按鈕沒有禁用,狀態標籤也沒有改變,在4秒以內窗體也沒法移動。
這個奇怪行爲的緣由其實很是簡單。下圖展現了這種情形。點擊按鈕時,按鈕的Click消息放入消息隊列。消息泵從隊列中移除該消息並開始處理點擊按鈕的處理程序代碼,即btnDoStuff_Click
方法。btnDoStuff_Click
處理程序將咱們但願觸發的行爲的消息放入隊列,以下右圖所示。但在處理程序自己退出(即休眠4秒並退出)以前,這些消息都沒法執行。而後全部的行爲都發生了,但速度太快肉眼根本看不見。
可是,若是處理程序能將前兩條消息壓入隊列,而後將本身從處理器上摘下,在4秒以後再將本身壓入隊列,那麼這些以及全部其餘消息均可以在等待的時間內被處理,整個過程就會如咱們以前預料的那樣,而且還能保持響應。
咱們可使用async/await
特性輕鬆地實現這一點,以下面修改的處理程序代碼。當到達await
語句時,處理程序返回到調用方法,並從處理器上摘下。這時其餘消息得以處理——包括處理程序已經壓入隊列的那兩條。在空閒任務完成後(本例中爲Task.Delay
),後續部分(方法剩餘部分)又被從新安排到線程上。
private async void btnDoStuff_Click(object sender, RoutedEventArgs e ) { btnDoStuff.IsEnabled = false; lblStatus.Content = "Doing Stuff"; await Task.Delay( 4000 ); lblStatus.Content = "Not Doing Anything"; btnDoStuff.IsEnabled = true; }
Task.Yield
方法建立一個當即返回的awaitable。等待一個Yield
可讓異步方法在執行後續部分的同時返回到調用方法。能夠將其理解成離開當前的消息隊列,回到隊列末尾,讓處理器有時間處理其餘任務。
下面的示例代碼展現了一個異步方法,程序每執行某個循環1000次就移交一次控制權。每次執行Yield
方法,都會容許線程中的其餘任務得以執行。
static class DoStuff { public static async Task<int> FindSeriesSuw( int il ) { int sum = 0; for ( int i=0; i < il; i++ ) { sum += i; if ( i % 1000 == 0 ) await Task.Yield(); } return sum; } } class Program { static void Main() { Task<int> value = DoStuff.FindSeriesSuw( 1000000 ); CountBig( 100000 ); CountBig( 100000 ); CountBig( 100000 ); CountBig( 100000 ); Console.WriteLine( "Sum: {0}", value.Result ); Console.ReadKey(); } private static void CountBig( int p ) { for ( int i=0; i < p; i++) ; } }
輸出:
Sum: 1783293664
Yield
方法在GUI程序中很是有用,能夠中斷大量工做,讓其餘任務使用處理器。
到目前爲止,本章只介紹了異步方法。但我曾經說過,你還可使用異步匿名方法和異步Lambda表達式。這種構造尤爲適合那些只有不多工做的事件處理程序。下面的代碼片斷將一個Lambda表達式註冊爲一個按鈕點擊事件的事件處理程序。
startWorkButton.Click += async (sender,e )=> { //處理點擊處理程序工做 }
下面用一個簡短的WPF程序來展現其用法,下面爲後臺代碼:
using System.Threading.Tasks; using System.Windows; namespace AsyncLambda { public partial class MainMindow : Window { public MainMindow() { InitializdComponent(); startWorkButton.Click += async (sender,e)=> { SetGuiValues( false, "Work Started"); await DoSomeWork(); SetGuiValues( true, "Work Finished"); }; } private void SetGuiValues(bool buttonEnabled, string status) { startWorkButton.IsEnabled = buttonEnabled; workStartedTextBlock.Text = status; } private Task DoSomeWork() { return Task.Delay(2500); } } }
XAML文件中的標記以下:
<Window x:Class="AsyncLambda.MainWindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Title="Async Lambda" Height="115" Width="150"> <StackPanel> <TextBlock Name="workStartedTextBlock" Margin="10,10"/> <Button Name="startWorkButton" Content="Start Work" Width="100" Margin="4"/> </StackPanel> </Window>
咱們按部就班地介紹了async/await
組件。本節你將看到一個完整的WPF GUI程序,包含一個狀態條和取消操做。
以下圖所示,左邊爲示例程序的截圖。點擊按鈕,程序將開始處理並更新進度條。處理過程完成將顯示右上角的消息框。若是在處理完成前點擊Cancel按鈕,程序將顯示右下角的消息框。
咱們首先建立一個名爲WpfAwait的WPF應用程序。按以下的代碼修改MainWindow.xaml中的XAML標記:
<Window x:Class="WpfAwait.MainWindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Title="Process and Cancel" Height="150" Width="250"> <StackPanel> <Button Name="btnProcess" Width="100" Click="btnProcess_Click" HorizontalAlignment="Right" Margin="10,15,10,10">Process</Button> <Button Name="btnCancel" Width="100" Click="btnCancel_Click" HorizontalAlignment="Right" Margin="10,0">Cancel</Button> <ProgressBar Name="progressBar" Height="20" Width="200" Margin="10" HorizontalAlignment="Right"/> </StackPanel> </Window>
按以下的代碼修改後臺代碼文件MainWindow.xaml.cs:
using System.Threading; using System.Threading.Tasks; using System.Windows; namespace WpfAwait { public partial class MainMindow : Window { CancellationTokenSource _cancellationTokenSource; CancellationToken _cancellationToken; public MainMindow() { InitializeComponent(); } private async void btnProcess_Click( object sender, RoutedEventArgs e ) { btnProcess.IsEnabled = false; _cancellationTokenSource = new CancellationTokenSource(); _cancellationToken = _cancellationTokenSource.Token; int completedPercent = 0; for ( int i = 0; i < 10; i++) { if ( _cancellationToken.IsCancellationRequested ) break; try { await Task.Delay( 500, _cancellationToken ); completedPercent =( i + 1 ) * 10; } catch ( TaskCanceledException ex ) { completedPercent = i * 10; } progressBar.Value = completedPercent; } string message = _cancellationToken.IsCancellationRequested ? string.Format("Process was cancelled at {0}%.", completedPercent) :"Process completed normally."; MessageBox.Show( message, "Completion Status"); progressBar.Value = 0; btnProcess.IsEnabled = true; btnCancel.IsEnabled = true; } private void btnCancel_Click( object sender, RoutedEventArgs e ) { if ( !btnProcess.IsEnabled ) { btnCancel.IsEnabled = false; _cancellationTokenSource.Cancel(); } } } }
前面幾節介紹瞭如何使用async/await
特性來異步地處理任務。本節將學習另外一種實現異步工做的方式——即後臺線程。async/await
特性更適合那些須要在後臺完成的不相關的小任務。
但有時候,你可能須要另建一個線程,在後臺持續運行以完成某項工做,並不時地與主線程進行通訊。BackgroundWorker
類就是爲此而生。下圖展現了此類的主要成員。
DoWork
ProgressChanged
事件RunWorkerCompleted
事件RunWorkerAsync
方法獲取後臺線程而且執行DoWork
事件處理程序CancelAsync
方法把CancellationPending
屬性設置爲true
。DoWork
事件處理程序須要檢查這個屬性來決定是否應該中止處理DoWork
事件處理程序(在後臺線程)在但願向主線程彙報進度的時候,調用ReportProgress
方法要使用BackgroundWorker
類對象,須要寫以下的事件處理程序。第一個是必需的,由於它包含你但願在後臺線程執行的代碼,另外兩個是可選的,是否使用取決於程序須要。
DoWork
事件的處理程序包含你但願在後臺獨立線程上執行的代碼。
DoTheWork
的處理程序用漸變的方塊表示,代表它在獨立的線程中執行RunWorkerAsync
方法的時候觸發DoWork
事件ReportProgress
方法與主線程通訊。屆時將觸發ProgressChanged
事件,主線程能夠處理附加到ProgressChanged
事件上的處理程序RunWorkerCompleted
事件的處理程序應該包含後臺線程完成DoWork
亊件處理程序的執行以後須要執行的代碼。下演示了程序的結構,以及附加到BackgroundWorker
對象事件的事件處理程序。
這些事件處理程序的委託以下。每個任務都有一個object
對象的引用做爲第一個參數,以及EventArgs
類的特定子類做爲第二個參數。
void DoWorkEventHandler ( object sender, DoWorkEventArgs e ) void ProgressChangedEventHandler ( object sender, ProgressChangedEventArgs e ) void RunWorkerCompletedEventHandler ( object sender, RunWorkerCompletedEventArgs e)
下圖演示了這些事件處理程序的EventArg
類的結構。
若是你編寫了這些事件處理程序並將其附加到相應的事件,就能夠這樣使用這些類。
BackgroundWorker
類的對象而且對它進行配置開始
WorkerReportsProgress
屬性設置爲true
WorkerSupportsCancellation
屬性設置爲true
RunWorkerAsync
方法來啓動它。它會開一個後臺線程而且發起DoWork
事件並在後臺執行事件處理程序如今咱們已經運行了主線程以及後臺線程。儘管後臺線程正在運行,你仍然能夠繼續主線程的處理。
在主線程中,若是你已經啓用了WorkerSupportsCancellation
屬性,而後能夠調用對象的CancelAsync
方法。和本章開頭介紹的CancellationToken
同樣,它也不會取消後臺線程。而是將對象的CancellationPending
屬性設置爲true
。運行在後臺線程中的DoWork
事件處理程序代碼須要按期檢査CancellationPending
屬性,來判斷是否須要退出。
同時在後臺線程繼續執行其計算任務,而且作如下幾件事情。
WorkerReportsProgress
屬性是true
而且後臺線程須要爲主線程彙報進度的話,必須調用BackgroundWorker
對象的ReportProgress
方法。這會觸發主線程的ProgressChanged
事件,從而運行相應的事件處理程序WorkerSupportsCancellation
屬性啓用的話,DoWork
事件處理程序代碼應該常常檢測CancellationPending
屬性來肯定是否已經取消了。若是是的話,則應該退出DoWorkEventArgs
參數的Result
字段來返回結果給主線程,這在上圖中已經說過了。在後臺線程退出的時候會觸發RunWorkerCompleted
事件,其事件處理程序在主線程上執行。RunWorkerCompletedEventArgs
參數能夠包含已完成後臺線程的一些信息,好比返回值以及線程是否被取消了。
BackgroundWorker類主要用於GUI程序,下面的程序展現了一個簡單的WPF程序。
該程序會生成下圖中左圖所示的窗體。點擊Process按鈕將開啓後臺線程,每半秒向主線程報告一次,並使進度條增加10%。最終,將展現右圖所示的對話框。
要建立這個WPF程序,須要在Visual Studio中建立名爲SimpleWorker的WPF應用程序。將MainWindow.xaml文件中的代碼修改成:
<Window x:Class="SimpleWorker.MainMindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Title="MainWindow" Height="l50" Width="250"> <StackPanel> <ProgressBar Name="progressBar" Height="20" Width="200" Margin="10"/> <Button Name="btnProcess" Width="l00" Click="btnProcess_Click" Margin="5">Process</Button> <Button Name="btnCancel" Width="l00" Click="btnCancel_Click" Margin="5">Cancel</Button> </StackPanel> </Window>
將MainWindow.xaml.cs文件中的代碼修改成:
using System.Windows; using System.ComponentModel; using System.Threading; namespace SimpleWorker { public partial class MainWindow : Window { BackgroundWorker bgWorker = new BackgroundWorker(); public MainWindow() { InitializeComponent(); //設置BackgroundWorker 屬性 bgWorker.WorkerReportsProgress = true; bgWorker.WorkerSupportsCancellation = true; //鏈接BackgroundWorker對象的處理程序 bgWorker.DoWork += DoWork_Handler; bgWorker.ProgressChanged += ProgressChanged_Handler; bgWorker.RunWorkerCompleted += RunWorkerCompleted_Handler; } private void btnProcess_Click( object sender, RoutedEventArgs e) { if ( !bgWorker.IsBusy ) bgWorker.RunWorkerAsync(); } private void ProgressChanged_Handler( object sender,ProgressChangedEventArgs args ) { progressBar.Value = args.ProgressPercentage; } private void DoWork_Handler( object sender, DoWorkEventArgs args ) { BackgroundWorker worker = sender as BackgroundWorker; for ( int i = 1; i <= 10; i++ ) { if ( worker.CancellationPending ) { args.Cancel = true; break; } else { worker.ReportProgress( i * 10 ); Thread.Sleep( 500 ); } } } private void RunWorkerCompleted_Handler( object sender,RunWorkerCompletedEventArgs args ) { progressBar.Value = 0; if ( args.Cancelled ) MessageBox.Show( "Process was cancelled.", "Process Cancelled"); else MessageBox.Show( "Process completed normally.", "Process Completed" ); } private void btnCancel_Click( object sender, RoutedEventArgs e ) { bgWorker.CancelAsync(); } } }
本節將簡要介紹任務並行庫(Task Parellel Library)。它是BCL中的一個類庫,極大地簡化了並行編程。其細節比本章要介紹的多得多。因此,我在這裏只能經過介紹其中的兩個簡單的結構做爲開胃菜了,這樣你能夠快速並很容易地入門,它們是Parallel.For
循環和Parallel.ForEach
循環。這兩個結構位於System.Threading.Tasks
命名空間中。
至此,我相信你應該很熟悉C#的標準for和foreach循環了。這兩個結構很是廣泛,且極其強大。許多時候咱們的循環結構的每一次迭代依賴於以前那一次迭代的計算或行爲。但有的時候又不是這樣。若是迭代之間彼此獨立,而且程序運行在多核處理器的機器上,若能將不一樣的迭代放在不一樣的處理器上並行處理的話,將會獲益匪淺。Parallel.For
和Parallel.ForEach
結構就是這樣作的。
這些構造的形式是包含輸入參數的方法。Parallel.For
方法有12個重載,最簡單的簽名以下。
public static ParallelLoopResult.For( int fromInclusive, int toExclusive, Action body);
fromInclusive
參數是迭代系列的第一個整數toExclusive
參數是比迭代系列最後一個索引號大1的整數。也就是說,和表達式index<ToExclusive
—樣body
是接受單個輸入參數的委託,body
的代碼在每一次迭代中執行一次以下代碼是使用Parallel.For
結構的例子。它從0到14迭代(記住實際的參數15超出了最大迭代索引)而且打印出迭代索引和索引的平方。該應用程序知足各個迭代之間是相互獨立的條件。還要注意,必須使用System.Threading.Tasks
命名空間。
using System; using System.Threading.Tasks; // 必須使用這個命名空間 namespace ExampleParallelFor { class Program { static void Main() { Parallel.For(0,15,i=> Console.WriteLine("The square of {0} is {1}",i,i*i)); } } }
在一個四核處理器的PC上運行這段代碼產生以下輸出。注意,不能確保迭代的執行次序。
The square of 0 is 0 The square of 6 is 36 The square of 7 is 49 The square of 8 is 64 The square of 10 is 100 The square of 11 is 121 The square of 13 is 169 The square of 14 is 196 The square of 4 is 16 The square of 5 is 25 The square of 1 is 1 The square of 2 is 4 The square of 9 is 81 The square of 12 is 144 The square of 3 is 9
另外一個示例以下。程序以並行方式填充一個整數數組,把值設置爲迭代索引號的平方。
class Program { static void Main() { const int maxValues=50; int[] squares=new int[maxValues]; Parallel.For(0,maxValues,i=>squares[i]=i*i); } }
在本例中,即便迭代在執行時可能爲並行而且爲任意順序。可是最後結果始終是一個包含前50個平方數的數組——而且按順序排列。
另一個並行循環結構是Parallel.ForEach
方法。該方法有至關多的重載,其中最簡單的以下:
TSource
是集合中對象的類型source
是一組TSource
對象的集合body
是要應用到集合中每個元素的Lambda表達式static ParallelLoopResult ForEach<TSource>( IEnumerable<TSource> source,Action<TSource> body)
使用Paralle.ForEach
方法的例子以下。在這裏,TSource
是string
,source
是string[]
。
using System; using System.Threading.Tasks; namespace ParallelForeach1 { class Program { static void Main() { string[] squares=new string[] {"We","hold","these","truths","to","be","self-evident","that","all","men","are","created","equal"}; Parallel.ForEach(squares, i=>Console.WriteLine(string.Format("{0} has {1} letters",i,i.Length))); } } }
在一個四核處理器的PC上運行這段代碼產生以下輸出,可是每一次運行均可能會有不同的順序。
We has 2 letters men has 3 letters truths has 6 letters self-evident has 12 letters equal has 5 letters are has 3 letters created has 7 letters to has 2 letters be has 2 letters hold has 4 letters these has 5 letters that has 4 letters all has 3 letters
若是咱們要本身編寫異步代碼,最可能使用的就是本章前面介紹的async/await
特性和BackgroundWorker
類,或者任務並行庫。然而,你仍然有可能須要使用舊的模式來產生異步代碼。爲了保持完整性,我將從如今開始介紹這些模式,直到本章結束。在學習了這些舊模式後,你將對async/await
特性是多麼簡單有更加深入的認識。
第13章介紹了委託的主題,而且瞭解到當委託對象調用時,它調用了它的調用列表中包含的方法。就像程序調用方法同樣,這是同步完成的。
若是委託對象在調用列表中只有一個方法(以後會叫作引用方法),它就能夠異步執行這個方法。委託類有兩個方法,叫作BeginInvoke
和EndInvoke
,它們就是用來這麼作的。這些方法以以下方式使用。
BeginInvoke
方法時,它開始在一個獨立線程上執行引用方法,而且當即返回到原始線程。原始線程能夠繼續,而引用方法會在線程池的線程中並行執行BeginInvoke
返回的IAsyncResult
的IsCompleted
屬性,或調用委託的EndInvoke
方法來等待委託完成下圖演示了使用這一過程的三種標準模式。對於這三種模式來講,原始線程都發起了一個異步方法,而後作一些其餘處理。然而,這些模式的區別在於,原始線程如何知道發起的線程已經完成。
EndInvoke
以前處理異步方法的結果在學習這些異步編程模式的示例以前,讓咱們先研究一下BeginInvoke
和EndInvoke
方法。一些須要瞭解的有關BeginInvoke
的重要事項以下。
BeginInvoke
時,參數列表中的實際參數組成以下
callback
參數和state
參數BeginInvoke
從線程池中獲取一個線程而且讓引用方法在新的線程中開始運行BeginInvoke
返回給調用線程一個實現IAsyncResult
接口的對象的引用。這個接口引用包含了在線程池線程中運行的異步方法的當前狀態,原始線程而後能夠繼續執行。以下的代碼給出了一個調用委託的BeginInvoke
方法的示例。第一行聲明瞭MyDel
委託類型。下一行聲明瞭一個和委託匹配的Sum
的方法。
del
的MyDel
委託類型的委託對象,而且使用Sum
方法來初始化它的調用列表BeginInvoke
方法而且提供了兩個委託參數3和5,以及兩個BeginInvoke
的參數callback
和state
,在本例中都設爲null
。執行後,BeginInvoke
方法進行兩個操做
Sum
方法,將3和5做爲實參IAsyncResult
接口的引用返回給調用線程來提供這些信息。調用線程把它保存在一個叫作iar的變量中delegate long MyDel(int first,int second);//委託聲明 ... static long Sum(int x,int y){...} //方法匹配委託 ... MyDel del=new MyDel(Sum); IAsyncResult iar=del.BeginInvoke(3,5,null,null);
EndInvoke
方法用來獲取由異步方法調用返回的值,而且釋放線程使用的資源。EndInvoke
有以下的特性。
BeginInvoke
方法返回的IAsyncResult
對象的引用,並找到它關聯的線程EndInvoke
作以下的事情
EndInvoke
被調用時線程池的線程仍然在運行,調用線程就會中止並等待,直到清理完畢並返回值。由於EndInvoke
是爲開啓的線程進行清理,因此必須確保對每個BeginInvoke
都調用EndInvoke
EndInvoke
時會拋出異常以下的代碼行給出了一個調用EndInvoke
並從異步方法獲取值的示例。咱們必須把IAsyncResult
對象的引用做爲參數。
委託對象 ↓ long result=del.EndInvoke(iar); ↑ ↑ 異步方法返回值 IAsyncResult對象
EndInvoke
提供了從異步方法調用的全部輸出,包括ref
和out
參數。若是委託的引用方法有ref
或out
參數,它們必須包含在EndInvoke
的參數列表中,而且在IAsyncResult
對象引用以前,以下所示:
long result=del.EndInvoke(out someInt,iar); ↑ ↑ ↑ 異步方法返回值 Out參數 IAsyncResult對象
既然咱們已經理解了BeginInvoke
和EndInvoke
方法,那麼就讓咱們來看看異步編程模式吧。
咱們要學習的第一種異步編程模式是等待一直到結束模式。在這種模式裏,原始線程發起一個異步方法的調用,作一些其餘處理,而後中止並等待,直到開啓的線程結束。它總結以下:
IAsyncResult iar = del.BeginInvoke( 3, 5, null, null ); //在發起線程中異步執行方法的同時, //在調用線程中處理一些其餘事情 ... long result = del.EndInvoke( iar );
以下代碼給出了一個使用這種模式的完整示例。代碼使用Thread
類的Sleep
方法將它本身掛起0.1秒。Thread
類在System.Threading
命名空間下。
using System; using System.Threading; // Thread.Sleep() delegate long MyDel( int first, int second ); //聲明委託類型 class Program { static long Sum(int x, int y) //聲明異步方法 { Console. WriteLine(" Inside Sum"); Thread.Sleep(100); return x + y; } static void Main( ) { MyDel del = new MyDel(Sum); Console.WriteLine( "Before BeginInvoke"); IAsyncResult iar = del.BeginInvoke(3, 5, null, null); //開擡異步調用 Console.WriteLine( "After BeginInvoke"); Console.WriteLine( "Doing stuff" ); long result = del.EndInvoke( iar ); //等待結果並獲取結果 Console.WriteLine( "After EndInvoke: {0}", result ); } }
等待一直到結束(wait-until-done)模式的輸出:
Before BeginInvoke After BeginInvoke Doing stuff Inside Sum After EndInvoke: 8
既然咱們已經看到了BeginInvoke
和EndInvoke
的最簡單形式,是時候來進一步接觸IASyncResult
了。它是使用這些方法的必要部分。
BeginInvoke
返回一個IASyncResult
接口的引用(內部是AsyncResult
類的對象)。AsyncResult
類表現了異步方法的狀態。下圖演示了該類中的一些重要部分。
有關該類的重要事項以下。
BeginInvoke
方法時,系統建立了一個AsyncResult
類的對象。然而,它不返回類對象的引用,而是返回對象中包含的IAsyncResult
接口的引用AsyncResult
對象包含一個叫作AsyncDelegate
的屬性,它返回一個指向被調用來開啓異步方法的委託的引用。可是,這個屬性是類對象的一部分而不是接口的一部分IsCompleted
屬性返回一個布爾值,表示異步方法是否完成AsyncState
屬性返回一個對象的引用,做爲BeginInvoke
方法調用時的state
參數。它返回object
類型的引用,咱們會在回調模式一節中解釋這部份內容在輪詢模式中,原始線程發起了異步方法的調用,作一些其餘處理,而後使用IAsyncResult
對象的IsComplete
屬性來按期檢査開後的線程是否完成。若是異步方法已經完成,原始線程就調用EndInvoke
並繼續。不然,它作一些其餘處理,而後過一下子再檢査。在下面的示例中,「處理」 僅僅是由0數到10 000 000。
delegate long MyDel(int first, int second); class Program { static long Sum(int x, int y) { Console.WriteLine(" Inside Sum"); Thread.Sleep(100); return x + y; } static void Main() { MyDel del = new MyDel(Sum);發起異步方法 ↓ IAsyncResult iar = del.BeginInvoke(3, 5, null, null); //開始異步謂用 Console.WriteLine("After BeginInvoke"); 檢查異步方法是否完成 ↓ while ( !iar.IsCompleted ) { Console.WriteLine("Not Done"); //繼續處理 for (long i = 0; i < 10000000; i++) ; } Console.WriteLine("Done"); 調用EndInvoke來獲取接口並進行清理 ↓ long result = del.EndInvoke(iar); Console.WriteLine("Result: {0}", result); } }
輪詢(polling)模式的輸出:
After BeginInvoke Not Done Inside Sum Not Done Not Done Not Done Not Done Done Result: 8
在以前的等待一直到結束(wait-until-done)模式以及輪詢(polling)模式中,初始線程繼續它本身的控制流程,直到它知道開啓的線程已經完成。而後,它獲取結果並繼續。
回調模式的不一樣之處在於,一旦初始線程發起了異步方法,它就本身管本身了,再也不考慮同步。當異步方法調用結束以後,系統調用一個用戶自定義的方法來處理結果,而且調用委託的EndInvoke
方法。這個用戶自定義的方法叫作回調方法或回調。
BeginInvoke
的參數列表中最後的兩個額外參數由回調方法使用。
callback
,是回調方法的名字state
,能夠是null
或要傳入回調方法的一個對象的引用。咱們能夠經過使用IAsyncResult
參數的AsyncState
屬性來獲取這個對象,參數的類型是object
回調方法的簽名和返回類型必須和AsyncCallback
委託類型所描述的形式一致。它須要方法接受一個IAsyncResult
做爲參數而且返回類型是void
,以下所示:
void AsyncCallback( IAsyncResult iar )
咱們有多種方式能夠爲BeginInvoke
方法提供回調方法。因爲BeginInvoke
中的callback
參數是AsyncCallback
類型的委託,咱們能夠以委託形式提供,以下面的第一行代碼所示。或者,咱們也能夠只提供回調方法名稱,讓編譯器爲咱們建立委託,兩種形式是徹底等價的。
使用回調方法建立委託 ↓ IAsyncResult iar1 =del.BeginInvoke(3, 5, new AsyncCallback(CallWhenDone), null); 只須要用回調方法的名字 ↓ IAsyncResult iar2 = del.BeginInvoke(3, 5, CallWhenDone, null);
BeginInvoke
的另外一個參數是發送給回調方法的對象。它能夠是任何類型的對象,可是參數類型是object
,因此在回調方法中,咱們必須轉換成正確的類型。
在回調方法內,咱們的代碼應該調用委託的EndInvoke
方法來處理異步方法執行後的輸出值。要調用委託的EndInvoke
方法,咱們確定須要委託對象的引用,而它在初始線程中,不在開啓的線程中。
若是不使用BeginInvoke
的state
參數做其餘用途,可使用它發送委託的引用給回調方法,以下所示:
結合後面的實例看,不將委託對象做爲參數傳入也能夠在回調函數內部獲取
AsyncResult
類對象。
這樣看來這個位置更應該傳入須要在回調函數中處理或用到的其它對象。
委託對象 把委託對象做爲狀態參數 ↓ ↓ IAsyncResult iar = del.BeginInvoke(3, 5, CallWhenDone, del);
而後,咱們能夠從發送給方法做爲參數的IAsyncResult
對象中提取出委託的引用。以下面的代碼所示。
IAsyncResult
接口的引用。請記住,IAsyncResult
接口對象在內部就是AsyncResult
類對象IAsyncResult
接口沒有委託對象的引用,而封裝它的AsyncResult
類對象卻有委託對象的引用。因此,示例代碼方法體的第一行就經過轉換接口引用爲類類型來獲取類對象的引用。變量ar
如今就有類對象的引用AsyncDelegate
屬性而且把它轉化爲合適的委託類型。這樣就獲得了委託引用,咱們能夠用它來調用EndInvoke
using System.Runtime.Remoting.Messaging; //包含AsyncResult類 void CallWhenDone( IAsyncResult iar ) { AsyncResult ar = (AsyncResult) iar; MyDel del = (MyDel) ar.AsyncDelegate; //獲取委託的引用 long Sum = del.EndInvoke( iar ); //調用 EndInvoke ... }
下面把全部知識點放在一塊兒,給出一個使用回調模式的完整示例。
using System; using System.Runtime.Remoting.Messaging;//調用AsyncResult類庫 using System.Threading; delegate long MyDel(int first, int second); class Program { static long Sum(int x, int y) { Console.WriteLine(" Inside Sum"); Thread.Sleep(100); return x + y; } static void CallWhenDone(IAsyncResult iar) { Console.WriteLine(" Inside CallWhenDone."); AsyncResult ar = (AsyncResult) iar; MyDel del = (MyDel)ar.AsyncDelegate; long result = del.EndInvoke(iar); Console.WriteLine(" The result is: {0}.",result); } static void Main() { MyDel del = new MyDel(Sum); Console.WriteLine("Before BeginInvoke"); IAsyncResult iar =del.BeginInvoke(3, 5, new AsyncCallback(CallWhenDone), null); Console.WriteLine("Doing more work in Main."); Thread.Sleep(500); Console.WriteLine("Done with Main. Exiting."); Console.ReadKey(); } }
回調(callback)模式的輸出:
Before BeginInvoke Doing more work in Main. Inside Sum Inside CallWhenDone. The result is: 8. Done with Main. Exiting.
計時器提供了另一種按期地重複運行異步方法的方式。儘管在.NET BCL中有好幾個可用的Timer
類,但在這裏咱們只會介紹System.Threading
命名空間中的那個。
有關計時器類須要瞭解的重要事項以下。
TimerCallback
委託形式的,結構以下所示。它接受一個object
類型做爲參數,而且返回類型是void
void TimerCallback( object state )
state
對象做爲其參數,而且開始運行dueTime
是回調方法首次被調用以前的時間。若是dueTime
被設爲特殊的值Timeout.Infinite
,則計時器不會開始。若是被設置爲0
,回調函數會被當即調用period
是兩次成功調用回調函數之間的時間間隔。若是它的值設置爲Timeout.Infinite
,回調在首次被調用以後不會再被調用state
能夠是null
或在每次回調方法執行時要傳入的對象的引用Timer
類的構造函數接受回調方法名稱、dueTime
、period
以及state
做爲參數。Timer
有不少構造函數,最爲經常使用的形式以下:
Timer(TimerCallback callback,object state,uint dueTime,uint period)
例:建立Timer
對象的示例:
回調的 在2000毫秒後 名字 第一次調用 ↓ ↓ Timer myTimer = new Timer ( MyCallback, someObject, 2000, 1000 ); ↑ ↑ 傳給回調的 每1000毫秒 對象 調用一次
一旦Timer
對象被建立,咱們可使用Change
方法來改變它的dueTime
或period
方法。
以下代碼給出了一個使用計時器的示例。Main
方法建立一個計時器,2秒鐘以後它會首次調用回調,而後每隔1秒調用1次。回調方法只是輸出了包含它被調用的次數的消息。
using System; using System.Threading; namespace Timers { class Program { int TimesCalled = 0; void Display(object state) { Console.WriteLine("{0} {1}", (string)state, ++TimesCalled); } static void Main() { Program p = new Program(); Timer myTimer = new Timer //2s後第一次調用,每1s重複依次 (p.Display, "Processing timer event", 2000, 1000); Console.WriteLine("Timer started."); Console.ReadLine(); } } }
輸出:
Timer started. Processing timer event 1 Processing timer event 2 Processing timer event 3 Processing timer event 4 Processing timer event 5
.NET BCL還提供了幾個其餘計時器類,每個都有其用途。其餘計時器類以下所示。
System.Windows.Forms.Timer
這個類在Windows應用程序中使用,用來按期把WM_TIMER
消息放到程序的消息隊列中。當程序從隊列獲取消息後,它會在主用戶接口線程中同步處理,這對Windows應用程序來講很是重要System.Timers.Timer
這個類更復雜,它包含了不少成員,使咱們能夠經過屬性和方法來操做計時器。它還有一個叫作Elapsed
的成員事件,每次時間到期就會發起這個事件。這個計時器能夠運行在用戶接口線程或工做者線程上