Net 中很多的類接口設計的時候都考慮了多線程問題,簡化了多線程程序的開發。 不用自己去寫 WaitHandler 等這些底層的代碼。由於歷史的發展,這些類的接口設計有着三種不同的風格: EAP(*)、
APM(*)和 TPL。目前重點用 TPL。
EAP
EAP 是 Event-based Asynchronous Pattern( 基於事件的異步模型) 的簡寫, 類似於 Ajax 中的XmlHttpRequest, send 之后並不是處理完成了,而是在 onreadystatechange 事件中再通知處理完成。
public partial class Form1 : Form { public Form1() { InitializeComponent(); } private void button1_Click(object sender, EventArgs e) { WebClient client = new WebClient(); client.DownloadStringCompleted += Client_DownloadStringCompleted; client.DownloadStringAsync(new Uri("http://www.baidu.com")); ; } private void Client_DownloadStringCompleted(object sender, DownloadStringCompletedEventArgs e) { MessageBox.Show(e.Result); } }
優點是簡單,缺點是當實現復雜的業務的時候很麻煩,比如下載 A 成功后再下載 b,如果下載 b成功再下載 c,否則就下載 d。
EAP 的類的特點是:一個異步方法配一個***Completed 事件。 .Net 中基於 EAP 的類比較少。也有更好的替代品,因此了解即可。
AMP
APM(Asynchronous Programming Model)是.Net 舊版本中廣泛使用的異步編程模型。使用了 APM的異步方法會返回一個 IAsyncResult 對象, 這個對象有一個重要的屬性 AsyncWaitHandle, 他是一個
用來等待異步任務執行結束的一個同步信號。
public partial class Form1 : Form { public Form1() { InitializeComponent(); } private void button1_Click(object sender, EventArgs e) { FileStream fs = File.OpenRead("d:/1.txt"); byte[] buffer = new byte[16]; IAsyncResult aResult = fs.BeginRead(buffer, 0, buffer.Length, null, null); aResult.AsyncWaitHandle.WaitOne();//等待任務執行結束 MessageBox.Show(Encoding.UTF8.GetString(buffer)); fs.EndRead(aResult); } }
如果不加 aResult.AsyncWaitHandle.WaitOne() 那么很有可能打印出空白,因為 BeginRead只是“開始讀取”。調用完成一般要調用 EndXXX 來回收資源。APM 的特點是:方法名字以 BeginXXX 開頭,返回類型為 IAsyncResult, 調用結束后需要EndXXX。
.Net 中有如下的常用類支持 APM: Stream、 SqlCommand、 Socket 等。
APM 還是太復雜,了解即可。
TPL
TPL(Task Parallel Library)是.Net 4.0 之后帶來的新特性,更簡潔,更方便。現在在.Net平台下已經大面積使用。
private void button1_Click(object sender, EventArgs e) { FileStream fs = File.OpenRead("d:/1.txt"); byte[] buffer = new byte[16]; Task<int> task = fs.ReadAsync(buffer, 0, buffer.Length); task.Wait(); MessageBox.Show("讀取了" + task.Result + "個字節"); MessageBox.Show(Encoding.UTF8.GetString(buffer)); }
使用async和awit關鍵字
private async void button2_Click(object sender, EventArgs e) { FileStream fs = File.OpenRead("d:/1.txt"); byte[] buffer = new byte[16]; int len = await fs.ReadAsync(buffer, 0, buffer.Length); MessageBox.Show("讀取了" + len + "個字節"); MessageBox.Show(Encoding.UTF8.GetString(buffer)); }
注意方法中如果有 await,則方法必須標記為 async,不是所有方法都可以被輕松的標記為 async。 WinForm 中的事件處理方法都可以標記為 async、 MVC 中的 Action 方法也可以標記為 async、控制台的 Main 方法不能標記為 async。
TPL 的特點是:方法都以 XXXAsync 結尾,返回值類型是泛型的 Task<T>。
TPL 讓我們可以用線性的方式去編寫異步程序,不再需要像 EAP 中那樣搞一堆回調、邏輯跳來跳去了。 await 現在已經被 JavaScript 借鑒走了!
用 await 實現“先下載 A,如果下載的內容長度大於 100 則下載 B,否則下載 C”就很容易了
再看看 WebClient 的 TPL 用法:
private async void button3_Click(object sender, EventArgs e) { var wc = new WebClient(); string html = await wc.DownloadStringTaskAsync("http://www.baidu.com");//不要丟了 await MessageBox.Show(html); //上面的代碼並不是完全等價於 WebClient wc1 = new WebClient(); var task = wc1.DownloadStringTaskAsync("http://www.baidu.com"); task.Wait(); MessageBox.Show(task.Result); // 因為如果按照上面的寫法,會卡死 UI 線程 //而 await 則 不 會 。。。 好 像 不 是 ? ? ? 那 只 是 因 為 把 html 這 么 長 的 字 符 串 //MessageBox.Show 很慢, MessageBox.Show(html.Substring(10)); 就證明了這一點 // Task<T> 中的 T 是什么類型每個方法都不一樣,要看文檔。 //WebClient、 Stream、 Socket 等這些“歷史悠久”的類都同時提供了 APM、 TPL 風格的 //API,甚至有的還提供了 EAP 風格的 API。盡可能使用 TPL 風格的 }
編寫異步方法
返回值為 Task<T>,潛規則(不要求)是方法名字以 Async 結尾:
static Task<string> F2Async() { return Task.Run(() => { System.Threading.Thread.Sleep(2000); return "F2"; }); }
調用:
private async void button4_Click(object sender, EventArgs e) { string s = await F2Async(); MessageBox.Show(s); }
TPL寫法
Task.Run()一個用來把一個代碼段包裝為 Task<T>的方法 Run 中委托的代碼體就是異步任務執行的邏輯,最后 return 返回值。
public partial class Form1 : Form { public Form1() { InitializeComponent(); } private async void button1_Click(object sender, EventArgs e) { /* string i1 = await F1Async(); MessageBox.Show("i1=" + i1); string i2 = await F2Async(); MessageBox.Show("i2=" + i2); */ //Task.Run()一個用來把一個代碼段包裝為 Task<T>的方法 Run 中委托的代碼體就是異步任 //務執行的邏輯,最后 return 返回值。 //把 button1_click 改成: Task<string> task1 = F1Async(); Task<string> task2 = F2Async(); string i1 = await task1; MessageBox.Show("i1=" + i1); string i2 = await task2; MessageBox.Show("i2=" + i2); } static Task<string> F1Async() { MessageBox.Show("F1 Start"); return Task.Run(() => { System.Threading.Thread.Sleep(1000); MessageBox.Show("F1 Run"); return "F1"; }); } static Task<string> F2Async() { MessageBox.Show("F2 Start"); return Task.Run(() => { System.Threading.Thread.Sleep(2000); MessageBox.Show("F2 Run"); return "F2"; }); } }
使用async返回值和Task.Run()返回值
static Task<string> GetRuPengAsync() { return Task.Run(() => { return "a"; }); //這里可以return Task<string> 類型的值,不能return “a”; } static async Task<string> GetRuPengAsync1() { return "a"; //這里不可以return Task<string> 類型的值, 而可以能return “a”; }
1、 只要方法是 Task<T>類型的返回值,都可以用 await 來等待調用獲取返回值
2、 如果一個返回 Task<T>類型的方法被標記了 async,那么只要方法內部直接 return T 這個類型的實例就可以。
3、 一個返回 Task<T>類型的方法沒有被標記了 async,那么需要方法內部直接 Task 實例
private async void button2_Click(object sender, EventArgs e) { //int i = await F1Async(); int i = await M2Async(); MessageBox.Show(i.ToString()); } static Task<int> M1Async() { return Task.Run(() => { return 2; }); } static async Task<int> M2Async() { return 2; }
TPL高級用法
1、 如果方法內部有 await,則方法必須標記為 async。 asp.net mvc 的 Action、 WinForm 的事件處理函數都可以標記 async,控制台 Main 不能 async。對於不能標記為怎么辦?
F1Async().Result 注意有的上下文下會有死鎖。
2、 如果返回值就是一個立即可以隨手可得的值,那么就用 Task.FromResult()如果是一個需要休息一會的任務(比如下載失敗則過 5 秒鍾后重試。主線程不休息,和Thread.Sleep 不一樣),
那么就用 Task.Delay()。
3、 Task.Factory.FromAsync()把 IAsyncResult 轉換為 Task, 這樣 APM 風格的 api 也可以用 await 來調用。
4、 編寫異步方法的簡化寫法。如果方法聲明為 async,那么可以直接 return 具體的值,不再用創建Task,由編譯器創建 Task:
static async Task<int> F1Async() { return 1; } //2、 如果返回值就是一個立即可以隨手可得的值,那么就用 Task.FromResult() static Task<int> F2Async() { return Task.FromResult(3); }
static Task<int> F3Async() { return Task.Run(() => { return 1 + 3; }); }
WinForm 程序依次下載三個網址: Task.WaitAll(task1, task2, task3);Task.WaitAll 是等待所有任務完成:
private async void button2_Click(object sender, EventArgs e) { /* HttpClient wc = new HttpClient(); string s1 = await wc.GetStringAsync(textBox1.Text); label1.Text = s1.Length.ToString(); string s2 = await wc.GetStringAsync(textBox2.Text); label2.Text = s2.Length.ToString(); string s3 = await wc.GetStringAsync(textBox3.Text); label3.Text = s3.Length.ToString(); */ HttpClient hc = new HttpClient(); var task1 = hc.GetStringAsync(textBox1.Text); var task2 = hc.GetStringAsync(textBox2.Text); var task3 = hc.GetStringAsync(textBox3.Text); Task.WaitAll(task1, task2, task3); label1.Text = task1.Result.Length.ToString(); label2.Text = task2.Result.Length.ToString(); label3.Text = task3.Result.Length.ToString(); }
TPL異常處理
1、 TPL 中,如果程序中出現異常,除非進行 try...catch,否則有可能是感覺不到出了異常的。測試,把上面下載程序的域名改成一個不存在的域名。
2、 TPL 程序有時候還會拋出 AggregateException, 這通常發生在並行有多個任務執行的情況下。 比如:
private async void button3_Click(object sender, EventArgs e) { try { HttpClient hc = new HttpClient(); var task1 = hc.GetStringAsync(textBox1.Text); var task2 = hc.GetStringAsync(textBox2.Text); var task3 = hc.GetStringAsync(textBox3.Text); Task.WaitAll(task1, task2, task3); label1.Text = task1.Result.Length.ToString(); label2.Text = task2.Result.Length.ToString(); label3.Text = task3.Result.Length.ToString(); } catch (AggregateException ae) { MessageBox.Show(ae.GetBaseException().ToString()); } }
因為多個並行的任務可能有多個有異常,因此會包裝為 AggregateException 異常,AggregateException 的 InnerExceptions 屬性可以獲得多個異常對象信息