這篇文章包含如下內容web
所謂異步,對於計算密集型的任務,是以線程爲基礎的,而在具體使用中,使用線程池裏面的線程仍是新建獨立線程,取決於具體的任務量;對於 I/O
密集型任務的異步,是以 Windows
事件爲基礎的算法
.NET
提供了執行異步操做的三種方式:編程
APM
) 模式(也稱 IAsyncResult
模式):在此模式中異步操做須要 Begin
和 End
方法(好比用於異步寫入操做的 BeginWrite
和 EndWrite
)。不建議新的開發使用此模式EAP
):這種模式須要一個或多個事件、事件處理程序委託類型和 EventArg
派生類型,以便在工做完成時觸發。不建議新的開發使用這種模式TAP
):它是在 .NET 4
中引入的。C#
中的 async
和 await
關鍵字爲 TAP
提供了語言支持。這是推薦使用方法因爲異步編程模型 (APM
) 模式與基於事件的異步模式 (EAP
)在新的開發中已經不推薦使用。故在此處咱們就不介紹了,如下僅介紹基於任務的異步模式(TAP
)數組
任務是工做的異步抽象,而不是線程的抽象。即當一個方法返回了 Task
或 Task<T>
,咱們不該該認爲它必定建立了一個線程,而是開始了一個任務。這對於咱們理解 TAP
是很是重要的。服務器
TAP
以 Task
和 Task<T>
爲基礎。它把具體的任務抽象成了統一的使用方式。這樣,不管是計算密集型任務,仍是 I/O
密集型任務,咱們均可以使用 async
、await
關鍵字來構建更加簡潔易懂的代碼網絡
任務分爲 計算密集型任務和 I/O密集型任務任務兩種dom
await
一個操做時,該操做會經過 Task.Run
方法啓動一個線程來處理相關的工做Task.Factory.StartNew
指定 TaskCreateOptions.LongRunning
選項 可使新的任務運行於獨立的線程上,而非使用線程池裏面的線程await
一個操做時,它將返回 一個 Task
或 Task<T>
。雖然計算密集型任務和 I/O
密集型任務在使用方式上沒有多大的區別,但其底層實現卻大不相同。異步
那咱們如何區分 I/O
密集型任務和計算密集型任務呢?
好比網絡操做,須要從服務器下載咱們所需的資源,它就是屬於 I/O
密集型的操做;好比咱們經過排序算法對一個數組排序時,這時的任務就是計算密集型任務。
簡而言之,判斷一個任務是計算型仍是 I/O
型,就看它佔用的 CPU
資源多,仍是 I/O
資源多就能夠了。async
對於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;
}
}
複製代碼
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;
}
複製代碼
對於前面咱們提到的下載電影的例子:獲取到速度最快的渠道以後,當即取消其餘的任務。如今咱們能夠這樣作
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
的一些基本的用法就介紹到這兒了
至此,本節內容講解完畢。下一篇文章咱們將講解 .NET
中的並行編程。歡迎關注公衆號【嘿嘿的學習日記】,全部的文章,都會在公衆號首發,Thank you~