這篇文章包含以下內(nèi)容
- 異步基礎(chǔ)
- 基于任務(wù)的異步模式
- 部分 API 介紹
異步基礎(chǔ)
所謂異步蠢涝,對(duì)于計(jì)算密集型的任務(wù)派阱,是以線程為基礎(chǔ)的,而在具體使用中揩环,使用線程池里面的線程還是新建獨(dú)立線程搔弄,取決于具體的任務(wù)量;對(duì)于 I/O
密集型任務(wù)的異步丰滑,是以 Windows
事件為基礎(chǔ)的
.NET
提供了執(zhí)行異步操作的三種方式:
- 異步編程模型 (
APM
) 模式(也稱IAsyncResult
模式):在此模式中異步操作需要Begin
和End
方法(比如用于異步寫入操作的BeginWrite
和EndWrite
)顾犹。不建議新的開發(fā)使用此模式 - 基于事件的異步模式 (
EAP
):這種模式需要一個(gè)或多個(gè)事件、事件處理程序委托類型和EventArg
派生類型褒墨,以便在工作完成時(shí)觸發(fā)炫刷。不建議新的開發(fā)使用這種模式 - 基于任務(wù)的異步模式 (
TAP
):它是在.NET 4
中引入的。C#
中的async
和await
關(guān)鍵字為TAP
提供了語(yǔ)言支持郁妈。這是推薦使用方法
由于異步編程模型 (APM
) 模式與基于事件的異步模式 (EAP
)在新的開發(fā)中已經(jīng)不推薦使用浑玛。故在此處我們就不介紹了,以下僅介紹基于任務(wù)的異步模式(TAP
)
基于任務(wù)的異步模式(TAP)
任務(wù)是工作的異步抽象噩咪,而不是線程的抽象锄奢。即當(dāng)一個(gè)方法返回了 Task
或 Task<T>
,我們不應(yīng)該認(rèn)為它一定創(chuàng)建了一個(gè)線程剧腻,而是開始了一個(gè)任務(wù)拘央。這對(duì)于我們理解 TAP
是非常重要的。
TAP
以 Task
和 Task<T>
為基礎(chǔ)书在。它把具體的任務(wù)抽象成了統(tǒng)一的使用方式灰伟。這樣,不論是計(jì)算密集型任務(wù),還是 I/O
密集型任務(wù)栏账,我們都可以使用 async
帖族、await
關(guān)鍵字來(lái)構(gòu)建更加簡(jiǎn)潔易懂的代碼
任務(wù)分為 計(jì)算密集型任務(wù)和 I/O密集型任務(wù)任務(wù)兩種
-
計(jì)算密集型任務(wù):當(dāng)我們
await
一個(gè)操作時(shí),該操作會(huì)通過Task.Run
方法啟動(dòng)一個(gè)線程來(lái)處理相關(guān)的工作
工作量大的任務(wù)挡爵,通過為Task.Factory.StartNew
指定TaskCreateOptions.LongRunning
選項(xiàng) 可以使新的任務(wù)運(yùn)行于獨(dú)立的線程上竖般,而非使用線程池里面的線程 -
I/O 密集型任務(wù):當(dāng)我們
await
一個(gè)操作時(shí),它將返回 一個(gè)Task
或Task<T>
茶鹃。
值得注意的是涣雕,這兒并不會(huì)啟動(dòng)一個(gè)線程
雖然計(jì)算密集型任務(wù)和 I/O
密集型任務(wù)在使用方式上沒有多大的區(qū)別,但其底層實(shí)現(xiàn)卻大不相同闭翩。
那我們?nèi)绾螀^(qū)分 I/O
密集型任務(wù)和計(jì)算密集型任務(wù)呢挣郭?
比如網(wǎng)絡(luò)操作,需要從服務(wù)器下載我們所需的資源疗韵,它就是屬于 I/O
密集型的操作兑障;比如我們通過排序算法對(duì)一個(gè)數(shù)組排序時(shí),這時(shí)的任務(wù)就是計(jì)算密集型任務(wù)蕉汪。
簡(jiǎn)而言之流译,判斷一個(gè)任務(wù)是計(jì)算型還是 I/O
型,就看它占用的 CPU
資源多者疤,還是 I/O
資源多就可以了福澡。
對(duì)于I/O
密集型的應(yīng)用,它們是以 Windows
事件為基礎(chǔ)的宛渐,因此不需要新建一個(gè)線程或使用線程池里面的線程來(lái)執(zhí)行具體工作。但我們?nèi)匀豢梢允褂?async
眯搭、await
來(lái)進(jìn)行異步處理窥翩,這得益于 .Net 為我們提供了一個(gè)統(tǒng)一的使用方式: Task
或 Task<T>
舉個(gè)例子,對(duì)于 I/O
密集型任務(wù)鳞仙,使用方式如下
// 這是在 .NET 4.5 及以后推薦的網(wǎng)絡(luò)請(qǐng)求方式
HttpClient httpClient = new HttpClient();
var result = await httpClient.GetStringAsync("https://www.baidu.com");
// 而不是以下這種方式(雖然得到的結(jié)果相同寇蚊,但性能卻不一樣,并且在.NET 4.5及以后都不推薦使用)
WebClient webClient = new WebClient();
var resultStr = Task.Run(() => {
return webClient.DownloadString("https://www.baidu.com");
});
對(duì)于計(jì)算密集型應(yīng)用棍好,使用方式如下
Random random = new Random();
List<int> data = new List<int>();
for (int i = 0; i< 50000000; i++) {
data.Add(random.Next(0, 100000));
}
// 這兒會(huì)啟動(dòng)一個(gè)線程仗岸,來(lái)執(zhí)行排序這種計(jì)算型任務(wù)
await Task.Run(() => {
data.Sort();
});
異步方法返回 Task
或 Task<TResult>
,具體取決于相應(yīng)方法返回的是 void
還是類型 TResult
借笙。如果返回的是 void
扒怖,則使用 Task
,如果是 TResult
业稼,則使用 Task<TResult>
不應(yīng)該使用
out
或ref
的方式來(lái)返回值盗痒,因?yàn)檫@可能產(chǎn)生意料之外的結(jié)果。因此低散,我們應(yīng)該盡可能的使用Task<TResult>
中的TResult
來(lái)組合多個(gè)返回值
另外俯邓,await不能用在返回值為 void 的方法上骡楼,否則會(huì)有編譯錯(cuò)誤
針對(duì) TAP
的編碼建議
-
async
與await
應(yīng)該搭配使用。即它們要么都出現(xiàn)稽鞭,要么都不出現(xiàn) - 僅在異步方法(即被
async
修飾的方法)中使用await
鸟整。否則會(huì)有編譯器錯(cuò)誤 - 如果一個(gè)方法內(nèi)部,沒有使用
await
朦蕴,則該方法不應(yīng)該使用async
來(lái)修飾篮条,否則會(huì)有編譯器警告 - 如果一個(gè)方法為異步方法(被
async
修飾),則它應(yīng)該以Async
結(jié)尾 - 我們應(yīng)該使用非阻塞的方式來(lái)編寫等待任務(wù)結(jié)果的代碼:
使用await
梦重、await Task.WhenAny
兑燥、await Task.WhenAll
、await Task.Delay
去等待后臺(tái)任務(wù)的結(jié)果琴拧。
而不是Task.Wait
降瞳、Task.Result
、Task.WaitAny
蚓胸、Task.WaitAll
挣饥、Thread.Sleep
,因?yàn)檫@些方式會(huì)阻塞當(dāng)前線程沛膳。
即如果需要等待或暫停扔枫,我們應(yīng)該使用.NET 4.5
提供的await
關(guān)鍵字,而不是使用.NET 4.5
之前的版本提供的方式 - 如果是計(jì)算密集型任務(wù)锹安,則應(yīng)該使用
Task.Run
來(lái)執(zhí)行任務(wù)短荐;如果是耗時(shí)比較長(zhǎng)的任務(wù),則應(yīng)該使用Task.Factory.StartNew
并指定TaskCreateOptions.LongRunning
選項(xiàng)來(lái)執(zhí)行任務(wù) - 如果是
I/O
密集型任務(wù)叹哭,不應(yīng)該使用Task.Run
忍宋。
因?yàn)?Task.Run
會(huì)在一個(gè)單獨(dú)的線程中運(yùn)行(線程池或者新建一個(gè)獨(dú)立線程),而對(duì)于I/O
任務(wù)來(lái)說风罩,啟用一個(gè)線程意義不大糠排,反而會(huì)浪費(fèi)線程資源
創(chuàng)建任務(wù)
要?jiǎng)?chuàng)建一個(gè)計(jì)算密集型任務(wù),在 .NET 4.5
及以后超升,可采用 Task.Run
的方式來(lái)快速創(chuàng)建入宦;如果需要對(duì)任務(wù)有更多的控制權(quán),則可以使用 .NET 4.0
提供的 Task.Factory.StartNew
來(lái)創(chuàng)建一個(gè)任務(wù)室琢。
對(duì)于 I/O
密集型任務(wù)乾闰,我們可以通過將 await
作用于對(duì)應(yīng)的 I/O
操作方法上即可
取消任務(wù)
在 TAP
中,任務(wù)是可以取消的盈滴。通過 CancellationTokenSource
來(lái)管理汹忠。需要支持取消的任務(wù),必須持有 CancellationTokenSource.Token
(令牌),以便該任務(wù)可以通過 CancellationTokenSource.Cancel()
的方式來(lái)取消宽菜。
使用 CancellationTokenSource
來(lái)取消任務(wù)谣膳,有以下優(yōu)點(diǎn)
- 可以將令牌傳遞給多個(gè)任務(wù),這樣可以同時(shí)取消多個(gè)任務(wù)铅乡。類似于一個(gè)老師继谚,可以管理多個(gè)學(xué)生。
- 可以通過
CancellationTokenSource.Token.Register
來(lái)監(jiān)聽任務(wù)的取消阵幸。這樣我們可以在任務(wù)取消之后做一些其他的工作
任務(wù)處理進(jìn)度
我們可以通過 IProgress<T>
接口監(jiān)聽進(jìn)度花履,如下所示
public Task ReadAsync(byte[] buffer, int offset, int count, IProgress<long> progress)
在 .NET 4.5
提供單個(gè) IProgress<T>
實(shí)現(xiàn):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;
}
舉個(gè)例子挚赊,假設(shè)我們需要獲取并顯示下載進(jìn)度诡壁,則可以按以下方式書寫
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
此方法可以幫助我們同時(shí)等待多個(gè)任務(wù),所有任務(wù)結(jié)束(正常結(jié)束荠割、異常結(jié)束)后返回
這里需要注意的是妹卿,如果單個(gè)任務(wù)有異常產(chǎn)生,這些異常會(huì)合并到 AggregateException
中蔑鹦。我們可以通過 AggregateException.InnerExceptions
來(lái)得到異常列表夺克;也可以使用 AggregateException.Handle
來(lái)對(duì)每個(gè)異常進(jìn)行處理,示例代碼如下
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 來(lái)得到內(nèi)部返回的異常
var exceptions = ex.InnerExceptions;
// 也可以使用 Handle 對(duì)每個(gè)異常進(jìn)行處理
ex.Handle(innerEx => {
// 此處的演示僅僅為了說明 ex.Handle 可以對(duì)異常進(jìn)行單獨(dú)處理
// 實(shí)際項(xiàng)目中不一定會(huì)拋出此異常
if (innerEx is OperationCanceledException oce) {
// 對(duì) OperationCanceledException 進(jìn)行單獨(dú)的處理
return true;
} else if (innerEx is UnauthorizedAccessException uae) {
// 對(duì) UnauthorizedAccessException 進(jìn)行單獨(dú)處理
return true;
}
return false;
});
}
}
但嚎朽,如果我們需要對(duì)每個(gè)任務(wù)進(jìn)行更加詳細(xì)的管理铺纽,則可以使用以下方式來(lái)處理
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) {
// 此處可以針對(duì)每個(gè)任務(wù)進(jìn)行更加具體的管理
foreach (Task<string> task in asyncOps) {
if (task.IsCanceled) {
}else if (task.IsFaulted) {
}else if (task.IsCompleted) {
}
}
}
}
這樣,就應(yīng)該基本上足夠應(yīng)對(duì)我們工作中的大部分的異常處理了
Task.WhenAny
與 Task.WhenAll
不同哟忍,Task.WhenAny
返回的是已完成的任務(wù)(可能只是所有任務(wù)中的幾個(gè)任務(wù))
舉個(gè)例子狡门,比如我們開發(fā)了一個(gè)圖片類App。我們可能需要在打開這個(gè)頁(yè)面時(shí),同時(shí)下載并展示多張圖片。但我們希望無(wú)論是哪一張圖片,只要下載完成,就展示出來(lái)吆倦,而不是所有的圖片都下載完了之后再展示。示例代碼如下
List<Task<Bitmap>> imageTasks = urls.Select(imgUrl => GetBitmapAsync(imgUrl)).ToList();
// 如果我們需要對(duì)圖片做一些處理(比如灰度化)米愿,可以使用以下代碼
// 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);
// 移除已經(jīng)下載完成的任務(wù)
imageTasks.Remove(imageTask);
// 同時(shí)將該任務(wù)的圖片威始,在UI上呈現(xiàn)出來(lái)
Bitmap image = await imageTask;
panel.AddImage(image);
} catch{}
}
Task.Delay
此方法用于暫停當(dāng)前任務(wù)的執(zhí)行,在指定時(shí)間之后繼續(xù)運(yùn)行肤寝。
它可以與 Task.WhenAny
和 Task.WhenAll
結(jié)合当辐,實(shí)現(xiàn)任務(wù)的超時(shí),如下
public async void btnDownload_Click(object sender, EventArgs e) {
btnDownload.Enabled = false;
try {
Task<Bitmap> download = GetBitmapAsync(url);
// 以下的這行代碼表示鲤看,如果在 3s 之內(nèi)沒有下載完成缘揪,則認(rèn)為超時(shí)
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;
}
}
通過這種方式,也可以監(jiān)聽使用 Task.WhenAll
時(shí)多個(gè)任務(wù)的超時(shí),如下
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));
}
另外找筝,提供兩個(gè)有用的函數(shù)蹈垢,以方便我們?cè)陧?xiàng)目中使用
RetryOnFail
定義如下所示
// 如果下載資源失敗后,我們希望重新下載時(shí)可以使用此方法
// 我們可以指定失敗之后袖裕,間隔多長(zhǎng)時(shí)間才重試曹抬。
// 也可以將 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;
}
對(duì)于前面我們提到的下載電影的例子:獲取到速度最快的渠道之后张足,立即取消其他的任務(wù)。現(xiàn)在我們可以這樣做
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)
);
以上提供的這兩個(gè)方法坎藐,在實(shí)際項(xiàng)目中會(huì)非常有用为牍,在需要時(shí)可以將它們用起來(lái)。當(dāng)然顺饮,通過對(duì) Task
的靈活運(yùn)用吵聪,可以組合出更多方便的方法出來(lái)。在具體項(xiàng)目中多多使用即可
關(guān)于 Task
的一些基本的用法就介紹到這兒了
至此兼雄,本節(jié)內(nèi)容講解完畢吟逝。下一篇文章我們將講解 .NET
中的并行編程。歡迎關(guān)注公眾號(hào)【嘿嘿的學(xué)習(xí)日記】赦肋,所有的文章块攒,都會(huì)在公眾號(hào)首發(fā),Thank you~