【转】温故之.NET 异步
轉自:https://zhuanlan.zhihu.com/p/38537169
這篇文章包含以下內容
- 異步基礎
- 基于任務的異步模式
- 部分 API 介紹
異步基礎
所謂異步,對于計算密集型的任務,就是以線程為基礎的多任務。而在具體使用中,使用線程池里面的線程還是新建獨立線程,取決于具體的任務量;對于?I/O?密集型任務的異步,是以?Windows?事件為基礎的。
.NET?提供了執行異步操作的三種方式:
- 異步編程模型 (APM) 模式(也稱?IAsyncResult?模式):在此模式中異步操作需要?Begin?和?End?方法(比如用于異步寫入操作的?BeginWrite?和?EndWrite)。不建議新的開發使用此模式
- 基于事件的異步模式 (EAP):這種模式需要一個或多個事件、事件處理程序委托類型和?EventArg?派生類型,以便在工作完成時觸發。不建議新的開發使用這種模式
- 基于任務的異步模式 (TAP):它是在?.NET 4?中引入的。C#?中的?async?和?await?關鍵字為?TAP?提供了語言支持。這是推薦使用方法
由于異步編程模型 (APM) 模式與基于事件的異步模式 (EAP)在新的開發中已經不推薦使用。故在此處我們就不介紹了,以下僅介紹基于任務的異步模式(TAP)
基于任務的異步模式(TAP)
任務是工作的異步抽象,而不是線程的抽象。即當一個方法返回了?Task?或?Task<T>,我們不應該認為它一定創建了一個線程,而是開始了一個任務。這對于我們理解?TAP?是非常重要的。
TAP?以?Task?和?Task<T>?為基礎。它把具體的任務抽象成了統一的使用方式。這樣,不論是計算密集型任務,還是?I/O?密集型任務,我們都可以使用?async?、await?關鍵字來構建更加簡潔易懂的代碼
任務分為?計算密集型任務和?I/O密集型任務任務兩種
- 計算密集型任務:當我們?await?一個操作時,該操作會通過?Task.Run?方法啟動一個線程來處理相關的工作
工作量大的任務,通過為?Task.Factory.StartNew?指定?TaskCreateOptions.LongRunning選項 可以使新的任務運行于獨立的線程上,而非使用線程池里面的線程 - I/O 密集型任務:當我們?await?一個操作時,它將返回 一個?Task?或?Task。
值得注意的是,這兒并不會啟動一個線程
雖然計算密集型任務和?I/O?密集型任務在使用方式上沒有多大的區別,但其底層實現卻大不相同。
那我們如何區分?I/O?密集型任務和計算密集型任務呢?
比如網絡操作,需要從服務器下載我們所需的資源,它就是屬于?I/O?密集型的操作;比如我們通過排序算法對一個數組排序時,這時的任務就是計算密集型任務。
簡而言之,判斷一個任務是計算型還是?I/O?型,就看它占用的?CPU?資源多,還是?I/O?資源多就可以了。
對于I/O密集型的應用,它們是以?Windows?事件為基礎的,因此不需要新建一個線程或使用線程池里面的線程來執行具體工作。但我們仍然可以使用?async、await?來進行異步處理,這得益于 .Net 為我們提供了一個統一的使用方式:?Task?或?Task<T>
舉個例子,對于?I/O?密集型任務,使用方式如下
// 這是在 .NET 4.5 及以后推薦的網絡請求方式 HttpClient httpClient = new HttpClient(); var result = await httpClient.GetStringAsync("https://www.baidu.com");// 而不是以下這種方式(雖然得到的結果相同,但性能卻不一樣,并且在.NET 4.5及以后都不推薦使用) WebClient webClient = new WebClient(); var resultStr = Task.Run(() => {return webClient.DownloadString("https://www.baidu.com"); });對于計算密集型應用,使用方式如下
Random random = new Random(); List<int> data = new List<int>(); for (int i = 0; i< 50000000; i++) {data.Add(random.Next(0, 100000)); } // 這兒會啟動一個線程,來執行排序這種計算型任務 await Task.Run(() => {data.Sort(); });異步方法返回?Task?或?Task<TResult>,具體取決于相應方法返回的是?void?還是類型?TResult。如果返回的是?void,則使用?Task,如果是?TResult,則使用?Task<TResult>
不應該使用?out?或?ref?的方式來返回值,因為這可能產生意料之外的結果。因此,我們應該盡可能的使用?Task<TResult>?中的?TResult?來組合多個返回值另外,await不能用在返回值為 void 的方法上,否則會有編譯錯誤
針對?TAP?的編碼建議
- async?與?await?應該搭配使用。即它們要么都出現,要么都不出現
- 僅在異步方法(即被?async?修飾的方法)中使用?await。否則會有編譯器錯誤
- 如果一個方法內部,沒有使用?await,則該方法不應該使用?async?來修飾,否則會有編譯器警告
- 如果一個方法為異步方法(被?async?修飾),則它應該以?Async?結尾
- 我們應該使用非阻塞的方式來編寫等待任務結果的代碼:
使用?await、await Task.WhenAny、?await Task.WhenAll、await Task.Delay?去等待后臺任務的結果。
而不是?Task.Wait?、Task.Result、Task.WaitAny、Task.WaitAll、Thread.Sleep,因為這些方式會阻塞當前線程。
即如果需要等待或暫停,我們應該使用?.NET 4.5?提供的?await?關鍵字,而不是使用?.NET 4.5?之前的版本提供的方式 - 如果是計算密集型任務,則應該使用?Task.Run?來執行任務;如果是耗時比較長的任務,則應該使用?Task.Factory.StartNew?并指定?TaskCreateOptions.LongRunning?選項來執行任務
- 如果是?I/O?密集型任務,不應該使用?Task.Run。
因為?Task.Run?會在一個單獨的線程中運行(線程池或者新建一個獨立線程),而對于?I/O?任務來說,啟用一個線程意義不大,反而會浪費線程資源
創建任務
要創建一個計算密集型任務,在?.NET 4.5?及以后,可采用?Task.Run?的方式來快速創建;如果需要對任務有更多的控制權,則可以使用?.NET 4.0?提供的?Task.Factory.StartNew?來創建一個任務。
對于?I/O?密集型任務,我們可以通過將?await?作用于對應的?I/O?操作方法上即可
取消任務
在?TAP?中,任務是可以取消的。通過?CancellationTokenSource?來管理。需要支持取消的任務,必須持有?CancellationTokenSource.Token?(令牌),以便該任務可以通過?CancellationTokenSource.Cancel()?的方式來取消。
使用?CancellationTokenSource?來取消任務,有以下優點
- 可以將令牌傳遞給多個任務,這樣可以同時取消多個任務。類似于一個老師,可以管理多個學生。
- 可以通過?CancellationTokenSource.Token.Register?來監聽任務的取消。這樣我們可以在任務取消之后做一些其他的工作
任務處理進度
我們可以通過?IProgress<T>?接口監聽進度,如下所示
public Task ReadAsync(byte[] buffer, int offset, int count, IProgress<long> progress)在?.NET 4.5?提供單個?IProgress<T>?實現:Progress<T>。Progress<T>?類的聲明方式如下:
// Progress<T> 類的聲明 public class Progress<T> : IProgress<T> { public Progress(); public Progress(Action<T> handler); protected virtual void OnReport(T value); public event EventHandler<T> ProgressChanged; }舉個例子,假設我們需要獲取并顯示下載進度,則可以按以下方式書寫
private async void btnDownload_Click(object sender, RoutedEventArgs e) { btnDownload.IsEnabled = false; try { txtResult.Text = await DownloadStringAsync(txtUrl.Text, new Progress<int>(p => pbDownloadProgress.Value = p)); } finally { btnDownload.IsEnabled = true; } }部分 API 介紹
Task.WhenAll
此方法可以幫助我們同時等待多個任務,所有任務結束(正常結束、異常結束)后返回
這里需要注意的是,如果單個任務有異常產生,這些異常會合并到?AggregateException?中。我們可以通過?AggregateException.InnerExceptions?來得到異常列表;也可以使用?AggregateException.Handle?來對每個異常進行處理,示例代碼如下
public static async void EmailAsync() {List<string> addrs = new List<string>();IEnumerable<Task> asyncOps = addrs.Select(addr => SendMailAsync(addr));try {await Task.WhenAll(asyncOps);} catch (AggregateException ex) {// 可以通過 InnerExceptions 來得到內部返回的異常var exceptions = ex.InnerExceptions;// 也可以使用 Handle 對每個異常進行處理ex.Handle(innerEx => {// 此處的演示僅僅為了說明 ex.Handle 可以對異常進行單獨處理// 實際項目中不一定會拋出此異常if (innerEx is OperationCanceledException oce) {// 對 OperationCanceledException 進行單獨的處理return true;} else if (innerEx is UnauthorizedAccessException uae) {// 對 UnauthorizedAccessException 進行單獨處理return true;}return false;});} }但,如果我們需要對每個任務進行更加詳細的管理,則可以使用以下方式來處理
public static async void EmailAsync() {List<string> addrs = new List<string>();IEnumerable<Task> asyncOps = addrs.Select(addr => SendMailAsync(addr));try {await Task.WhenAll(asyncOps);} catch (AggregateException ex) {// 此處可以針對每個任務進行更加具體的管理foreach (Task<string> task in asyncOps) {if (task.IsCanceled) {}else if (task.IsFaulted) {}else if (task.IsCompleted) {}}} }這樣,就應該基本上足夠應對我們工作中的大部分的異常處理了
Task.WhenAny
與?Task.WhenAll?不同,Task.WhenAny?返回的是已完成的任務(可能只是所有任務中的幾個任務)
舉個例子,比如我們開發了一個圖片類App。我們可能需要在打開這個頁面時,同時下載并展示多張圖片。但我們希望無論是哪一張圖片,只要下載完成,就展示出來,而不是所有的圖片都下載完了之后再展示。示例代碼如下
List<Task<Bitmap>> imageTasks = urls.Select(imgUrl => GetBitmapAsync(imgUrl)).ToList(); // 如果我們需要對圖片做一些處理(比如灰度化),可以使用以下代碼 // List<Task<Bitmap>> imageTasks = urls.Select(imgUrl => GetBitmapAsync(imgUrl).ContinueWith(task => ConvertToGray(task.Result)).ToList(); while(imageTasks.Count > 0) { try { Task<Bitmap> imageTask = await Task.WhenAny(imageTasks);// 移除已經下載完成的任務imageTasks.Remove(imageTask); // 同時將該任務的圖片,在UI上呈現出來Bitmap image = await imageTask; panel.AddImage(image); } catch{} }Task.Delay
此方法用于暫停當前任務的執行,在指定時間之后繼續運行。
它可以與?Task.WhenAny?和?Task.WhenAll?結合,實現任務的超時,如下
public async void btnDownload_Click(object sender, EventArgs e) { btnDownload.Enabled = false; try { Task<Bitmap> download = GetBitmapAsync(url); // 以下的這行代碼表示,如果在 3s 之內沒有下載完成,則認為超時if (download == await Task.WhenAny(download, Task.Delay(3000))) { Bitmap bmp = await download; pictureBox.Image = bmp; status.Text = "Downloaded"; } else { pictureBox.Image = null; status.Text = "Timed out"; var ignored = download.ContinueWith(t => Trace("Task finally completed"));} } finally { btnDownload.Enabled = true; } }通過這種方式,也可以監聽使用?Task.WhenAll?時多個任務的超時,如下
Task<Bitmap[]> downloads = Task.WhenAll(from url in urls select GetBitmapAsync(url)); if (downloads == await Task.WhenAny(downloads, Task.Delay(3000))) { foreach(var bmp in downloads) panel.AddImage(bmp); status.Text = "Downloaded"; } else {status.Text = "Timed out"; downloads.ContinueWith(t => Log(t)); }另外,提供兩個有用的函數,以方便我們在項目中使用
RetryOnFail
定義如下所示
// 如果下載資源失敗后,我們希望重新下載時可以使用此方法 // 我們可以指定失敗之后,間隔多長時間才重試。 // 也可以將 retryWhen 指定為 null,以便在失敗之后立即重試 public static async Task<T> RetryOnFail<T>(Func<Task<T>> function, int maxTries, Func<Task> retryWhen) {for (int i = 0; i < maxTries; i++) {try {return await function().ConfigureAwait(false);} catch {if (i == maxTries - 1) throw;}if (retryWhen != null)await retryWhen().ConfigureAwait(false);}return default(T); }使用方式如下,這在失敗之后,暫停 1s,然后再重試
string pageContents = await RetryOnFail(() => DownloadStringAsync(url), 3, () => Task.Delay(1000));或者如下,這將在失敗之后立即重試
string pageContents = await RetryOnFail(() => DownloadStringAsync(url), 3, null);NeedOnlyOne
定義如下
public static async Task<T> NeedOnlyOne<T>(params Func<CancellationToken, Task<T>>[] functions) {var cts = new CancellationTokenSource();var tasks = functions.Select(func => func(cts.Token));var completed = await Task.WhenAny(tasks).ConfigureAwait(false);cts.Cancel();foreach (var task in tasks) {var ignored = task.ContinueWith(t => Trace.WriteLine(t), TaskContinuationOptions.OnlyOnFaulted);}return await completed; }對于前面我們提到的下載電影的例子:獲取到速度最快的渠道之后,立即取消其他的任務?,F在我們可以這樣做
var line = await NeedOnlyOne(token => DetectSpeedAsync("line_1", movieName, cts.Token),token => DetectSpeedAsync("line_2", movieName, cts.Token),token => DetectSpeedAsync("line_3", movieName, cts.Token));以上提供的這兩個方法,在實際項目中會非常有用,在需要時可以將它們用起來。當然,通過對?Task?的靈活運用,可以組合出更多方便的方法出來。在具體項目中多多使用即可
關于?Task?的一些基本的用法就介紹到這兒了
至此,本節內容講解完畢。下一篇文章我們將講解?.NET?中的并行編程。
歡迎關注公眾號【嘿嘿的學習日記】,所有的文章,都會在公眾號首發,Thank you~
總結
以上是生活随笔為你收集整理的【转】温故之.NET 异步的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 中信大众点评信用卡额度一般多少
- 下一篇: 只有100元如何理财?100元短期理财方