現(xiàn)在小米2手機都已經(jīng)4核了,更別說PC機了 。
多核,并發(fā) ,異步編程是不可阻擋的趨勢, 各種語言也都逐漸加入了對異步編程的支持。
異步編程的本質(zhì)是將主線程某些任務(wù)委托給某個線程處理,主線程繼續(xù)無阻塞運行, 等另外一個線程處理完了再將結(jié)果通知主線程(在主線程中調(diào)用某個主線程的結(jié)果處理方法)。
本來很好理解的概念,但是因為匿名函數(shù)(Lambda)導(dǎo)致的閉包和多線程以及編譯器在背后做了很多事情,導(dǎo)致我們很難深入的理解它。
下文轉(zhuǎn)自http://blogs.msdn.com/b/windowsappdev_cn/archive/2012/04/30/winrt-await.aspx
在最近發(fā)布的使用 Windows 運行時中異步性來始終保持應(yīng)用程序能夠快速流暢地運行這篇博文中,包含了一些如何在 C# 和 Visual Basic 中使用 await 關(guān)鍵字的示例,允許開發(fā)人員在使用 WinRT 異步操作的同時,保持和推導(dǎo)良好的控制流。
在接下來的博文中,我將更加深入地介紹 await 在 WinRT 中的工作原理。這些知識將幫助您更輕松地推導(dǎo)使用 await 的代碼,進而幫助您編寫更出色的 Metro 風(fēng)格應(yīng)用程序。
首先,我們先來審視一下沒有 await 的情況。
基礎(chǔ)知識回顧
WinRT 中的所有異步功能全部源自同一個接口:IAsyncInfo。
public interface IAsyncInfo
{
AsyncStatus Status { get; }
HResult ErrorCode { get; }
uint Id { get; }
void Cancel();
void Close();
}
WinRT 中的每個異步操作都需要實施此接口,該接口可提供執(zhí)行異步操作所需的基本功能,查詢其標(biāo)識和狀態(tài),并請求其取消。但該特定接口缺少對異步操作來說無疑是至關(guān)重要的功能:當(dāng)操作完成時,通過回調(diào)通知監(jiān)聽器。該功能有意地劃分到了四個依賴 IAsyncInfo 的其他接口中,而 WinRT 中的每個異步操作都需要實施以下四個接口之一:
public interface IAsyncAction : IAsyncInfo
{
AsyncActionCompletedHandler Completed { get; set; }
void GetResults();
}
public interface IAsyncOperation<TResult> : IAsyncInfo
{
AsyncOperationCompletedHandler<TResult> Completed { get; set; }
TResult GetResults();
}
public interface IAsyncActionWithProgress<TProgress> : IAsyncInfo
{
AsyncActionWithProgressCompletedHandler<TProgress> Completed { get; set; }
AsyncActionProgressHandler<TProgress> Progress { get; set; }
void GetResults();
}
public interface IAsyncOperationWithProgress<TResult, TProgress> : IAsyncInfo
{
AsyncOperationWithProgressCompletedHandler<TResult, TProgress> Completed { get; set; }
AsyncOperationProgressHandler<TResult, TProgress> Progress { get; set; }
TResult GetResults();
}
這四個接口支持帶有/不帶結(jié)果,以及帶有/不帶進度報告的全部組合。所有這些接口都會公開一個 Completed 屬性,該屬性可以設(shè)置為在操作完成時調(diào)用的委派。您只能設(shè)置一次該委派,而如果該委派在操作完成后設(shè)置,它將通過處理操作完成和分配委派之間先后順序的實施,立即加入計劃或得到調(diào)用。
現(xiàn)在,我們假設(shè)要實施一個帶有 XAML 按鈕的 Metro 風(fēng)格應(yīng)用程序,單擊該按鈕可以將某些任務(wù)加入 WinRT 線程池,以執(zhí)行某種大計算量的操作。當(dāng)該任務(wù)完成時,按鈕的內(nèi)容將根據(jù)操作的結(jié)果進行更新。
我們應(yīng)該如何實施此功能呢?WinRT ThreadPool 類公開了一種可以在池中異步運行任務(wù)的方法:
public static IAsyncAction RunAsync(WorkItemHandler handler);
我們可以使用此方法將大計算量的工作加入隊列,以避免當(dāng)在任務(wù)運行時阻止 UI 線程:
private void btnDoWork_Click(object sender, RoutedEventArgs e)
{
int result = 0;
var op = ThreadPool.RunAsync(delegate { result = Compute(); });
}
我們現(xiàn)在已經(jīng)成功地將工作從 UI 線程分流到了池中,但我們?nèi)绾潍@知工作何時完成呢?RunAsync 會返回一個 IAsyncAction,因此我們可以使用一個完成處理程序來接收該通知,并運行續(xù)體作為響應(yīng):
private void btnDoWork_Click(object sender, RoutedEventArgs e)
{
int result = 0;
var op = ThreadPool.RunAsync(delegate { result = Compute(); });
op.Completed = delegate(IAsyncAction asyncAction, AsyncStatus asyncStatus)
{
btnDoWork.Content = result.ToString(); // bug!
};
}
現(xiàn)在,當(dāng)加入 ThreadPool 隊列的異步操作完成時,Completed 處理程序?qū)⒌玫秸{(diào)用,并嘗試將結(jié)果存儲到按鈕中。很不幸,這一操作目前無法成功執(zhí)行。Completed 處理程序不太可能在 UI 線程中得到調(diào)用,但是,為了修改 btnDoWork.Content,該處理程序需要在 UI 線程中運行(如果無法實現(xiàn),則將導(dǎo)致一個錯誤代碼為“RPC_E_WRONG_THREAD”的異常)。為此,我們可以使用與 UI 相關(guān)的CoreDispatcher 對象將調(diào)用封送回所需的位置:
private void btnDoWork_Click(object sender, RoutedEventArgs e)
{
int result = 0;
var op = ThreadPool.RunAsync(delegate { result = Compute(); });
op.Completed = delegate(IAsyncAction asyncAction, AsyncStatus asyncStatus)
{
Dispatcher.RunAsync(CoreDispatcherPriority.Normal, delegate
{
btnDoWork.Content = result.ToString();
});
};
}
現(xiàn)在,該應(yīng)用可以正常工作了。但如果 Compute 方法拋出異常會怎樣?如果某人調(diào)用返回自 ThreadPool.RunAsync 的IAsyncAction 中的 Cancel 又會怎樣?我們的 Completed 處理程序需要處理 IAsyncAction 可能會以下列三種終端狀態(tài)之一結(jié)束的事實:Completed、Error 或 Canceled:
private void btnDoWork_Click(object sender, RoutedEventArgs e)
{
int result = 0;
var op = ThreadPool.RunAsync(delegate { result = Compute(); })
op.Completed = delegate(IAsyncAction asyncAction, AsyncStatus asyncStatus)
{
Dispatcher.RunAsync(CoreDispatcherPriority.Normal, delegate
{
switch (asyncStatus)
{
case AsyncStatus.Completed:
btnDoWork.Content = result.ToString();
break;
case AsyncStatus.Error:
btnDoWork.Content = asyncAction.ErrorCode.Message;
break;
case AsyncStatus.Canceled:
btnDoWork.Content = "A task was canceled";
break;
}
});
};
}
為了處理一個異步調(diào)用,我們就需要編寫大量代碼;如果我們需要連續(xù)執(zhí)行多個異步操作,情形可想而知。如果我們能通過某種方法替代這種繁復(fù)的代碼編寫工作,豈不是非常美妙?
private async void btnDoWork_Click(object sender, RoutedEventArgs e)
{
try
{
int result = 0;
await ThreadPool.RunAsync(delegate { result = Compute(); });
btnDoWork.Content = result.ToString();
}
catch (Exception exc) { btnDoWork.Content = exc.Message; }
}
這段代碼的功能與前一段代碼完全相同。但我們不再需要手動處理完成回調(diào)。我們不再需要手動封送回 UI 線程。我們不再需要明確檢查完成狀態(tài)。并且我們不再需要顛倒控制流,這意味著我們可以輕而易舉地擴展至更多操作,例如,循環(huán)執(zhí)行多個計算和 UI 更新:
private async void btnDoWork_Click(object sender, RoutedEventArgs e)
{
try
{
for(int i=0; i<5; i++)
{
int result = 0;
await ThreadPool.RunAsync(delegate { result = Compute(); });
btnDoWork.Content = result.ToString();
}
}
catch (Exception exc) { btnDoWork.Content = exc.Message; }
}
請回想一下手動使用 IAsyncAction 時,為實現(xiàn)上述功能必須編寫的代碼量。這就是 C# 和 Visual Basic 中新的 async/await 關(guān)鍵字的魔力。好消息是您可以親手編寫這些代碼,它們并非難以捉摸的魔法。在本博文余下的篇幅中,我們將帶您深入了解這一功能的后臺原理。
編譯器轉(zhuǎn)換
使用 async 關(guān)鍵字標(biāo)記方法,會導(dǎo)致 C# 或 Visual Basic 編譯器使用狀態(tài)機重新編寫該方法的實施。借助此狀態(tài)機,編譯器可以在該方法中插入多個中斷點,以便該方法可以在不阻止線程的情況下,掛起和恢復(fù)其執(zhí)行。這些中斷點不會隨意地插入。它們只會在您明確使用await 關(guān)鍵字的位置插入:
private async void btnDoWork_Click(object sender, RoutedEventArgs e)
{
...
await something; // <-- potential method suspension point
...
}
當(dāng)您等待未完成的異步操作時,編譯器生成的代碼可確保與該方法相關(guān)的所有狀態(tài)(例如,局部變量)封裝并保留在堆中。然后,該函數(shù)將返回到調(diào)用程序,允許在其運行的線程中執(zhí)行其他任務(wù)。當(dāng)所等待的異步操作在稍后完成時,該方法將使用保留的狀態(tài)恢復(fù)執(zhí)行。
任何公開 await 模式的類型都可以進行等待。該模式主要由一個公開的 GetAwaiter 方法組成,該方法會返回一個提供IsCompleted、OnCompleted 和 GetResult 成員的類型。當(dāng)您編寫以下代碼時:
編譯器會生成一段代碼,在實例 something 上使用這些成員來檢查該對象是否已完成(通過 IsCompleted),如果未完成,則掛接一個續(xù)體(通過 OnCompleted),并在該任務(wù)最終完成時進行回調(diào)以繼續(xù)執(zhí)行。該操作完成后,來自該操作的任何異常將得到傳播或作為結(jié)果返回(通過 GetResult)。因此,當(dāng)您編寫以下代碼時:
private async void btnDoWork_Click(object sender, RoutedEventArgs e)
{
...
await ThreadPool.RunAsync(delegate { result = Compute(); });
...
}
編譯器會將其轉(zhuǎn)換為類似如下的代碼:
private class btnDoWork_ClickStateMachine : IAsyncStateMachine
{
// Member fields for preserving locals and other necessary state
int $state;
TaskAwaiter<string> $awaiter;
int result;
...
// Method that moves to the next state in the state machine
public void MoveNext()
{
// Jump table to get back to the right statement upon resumption
switch (this.$state)
{
...
case 2: goto Label2;
...
}
...
// Expansion of await ...;
var tmp1 = ThreadPool.RunAsync(delegate { this.result = Compute(); });
this.$awaiter = tmp1.GetAwaiter();
if (!this.$awaiter.IsCompleted)
{
this.$state = 2;
this.$awaiter.OnCompleted(MoveNext);
return;
Label2:
}
this.$awaiter.GetResult();
...
}
}
對于 btnDoWork_Click 方法,編譯器會生成一個包含 MoveNext 方法的狀態(tài)機類。對 MoveNext 的每次調(diào)用都會恢復(fù)btnDoWork_Click 方法的執(zhí)行,直到遇到下一個針對某項未完成任務(wù)的 await 語句,或直到該方法結(jié)束,二者取其先。當(dāng)編譯器生成的代碼發(fā)現(xiàn)未完成的所等待實例時,它會使用狀態(tài)變量標(biāo)記當(dāng)前的位置,安排方法在所等待實例完成時繼續(xù)執(zhí)行,然后返回。當(dāng)所等待實例最終完成時,系統(tǒng)將再次調(diào)用 MoveNext 方法,并跳轉(zhuǎn)至上次執(zhí)行中斷的位置。
編譯器事實上并不關(guān)心 IAsyncAction 是否在此處等待。它只關(guān)心是否有可供綁定的正確模式。當(dāng)然,您已經(jīng)看到了 IAsyncAction接口的實施,并且知道其中不包含編譯器所期待的 GetAwaiter 方法。那么,為什么它能夠成功進行編譯并運行呢?為了更好地理解其中的原因,您需要首先了解 .NET Task 和 Task<TResult> 類型(該框架用于表示異步操作的核心),及其與 await 的關(guān)系。
轉(zhuǎn)換為任務(wù)
.NET Framework 4.5 包含支持等待 Task 和 Task<TResult> 實例(Task<TResult> 派生自 Task)所需的全部類型和方法。Task 和 Task<TResult> 均公開 GetAwaiter 實例方法,并分別返回 TaskAwaiter 和 TaskAwaiter<TResult> 類型,這兩種類型均公開可滿足 C# 和 Visual Basic 編譯器需要的 IsCompleted、OnCompleted 和 GetResult 成員。IsCompleted 會返回一個布爾值,指示在訪問該屬性的時刻,任務(wù)是否已經(jīng)完成執(zhí)行。OnCompleted 會將任務(wù)掛接到一個續(xù)體委派,該委派會在任務(wù)完成時得到調(diào)用(如果當(dāng) OnCompleted 得到調(diào)用時該任務(wù)已經(jīng)完成,該續(xù)體委派將加入異步執(zhí)行計劃)。GetResult 會在以TaskStatus.RanToCompletion 狀態(tài)結(jié)束的情況下返回任務(wù)的結(jié)果(針對非泛型 Task 類型,返回 void);在以TaskStatus.Canceled 狀態(tài)結(jié)束的情況下,拋出 OperationCanceledException;并在以 TaskStatus.Faulted 狀態(tài)結(jié)束的情況下,拋出導(dǎo)致任務(wù)失敗的任何異常。
如果希望某種自定義類型支持等待,我們可以選擇兩種主要的方法。一種方法是針對自定義的可等待類型手動實施完整的 await 模式,提供一個返回自定義等待程序類型的 GetAwaiter 方法,該等待程序類型知道如何處理續(xù)體和異常傳播等等。第二種實施該功能的方法是將自定義類型轉(zhuǎn)換為任務(wù),然后只需依靠對等待任務(wù)的內(nèi)置支持來等待特殊類型。我們來探究一下后一種方法。
.NET Framework 包含一種名為 TaskCompletionSource<TResult> 的類型,可以讓此類轉(zhuǎn)換更加直觀。TaskCompletionSource<TResult> 會創(chuàng)建一個 Task<TResult> 對象,并向您提供用于直接控制相應(yīng)的任務(wù)將在何時、以何種狀態(tài)結(jié)束的 SetResult、SetException 和 SetCanceled 方法。因此,您可以將 TaskCompletionSource<TResult> 用作填充或代理來表示某些其他異步操作,例如,某種 WinRT 異步操作。
我們暫時假設(shè)您不知道自己可以直接等待 WinRT 操作。那么,您將如何來實現(xiàn)這些功能呢?
IAsyncOperation<string> op = SomeMethodAsync();
string result = await ...; // need something to await
您可以創(chuàng)建一個 TaskCompletionSource<TResult>,將其用作代理來表示該 WinRT 異步操作,然后等待相應(yīng)的任務(wù)。我們來嘗試一下。首先,我們需要實例化一個 TaskCompletionSource<TResult>,以便等待其 Task:
IAsyncOperation<string> op = SomeMethodAsync();
var tcs = new TaskCompletionSource<TResult>();
...
string result = await tcs.Task;
然后,如同我們在稍早前的示例中看到如何手動使用 WinRT 異步操作的 Completed 處理程序一樣,我們需要向該異步操作掛接一個回調(diào),以便獲知其何時完成:
IAsyncOperation<string> op = SomeMethodAsync();
var tcs = new TaskCompletionSource<TResult>();
op.Completed = delegate
{
...
};
string result = await tcs.Task;
然后在該回調(diào)中,我們需要將 IAsyncOperation<TResult> 的完成狀態(tài)傳輸給該任務(wù):
IAsyncOperation<string> op = SomeMethodAsync();
var tcs = new TaskCompletionSource<TResult>();
op.Completed = delegate
{
switch(operation.Status)
{
AsyncStatus.Completed:
tcs.SetResult(operation.GetResults());
break;
AsyncStatus.Error:
tcs.SetException(operation.ErrorCode);
break;
AsyncStatus.Canceled:
tcs.SetCanceled();
break;
}
};
string result = await tcs.Task;
就是這樣。即使處理程序在該操作已經(jīng)完成后才注冊,WinRT 異步操作也可確保 Completed 得到適當(dāng)?shù)恼{(diào)用,因此我們在注冊該處理程序時,無需針對其與該操作完成的先后順序進行特殊處理。WinRT 異步操作還負責(zé)在操作完成后將引用下降至 Completed 處理程序,因此我們無需進行任何特殊處理,以便在處理程序得到調(diào)用時將 Completed 設(shè)置為 null;事實上,Completed 處理程序只能設(shè)置一次,這意味著如果您再次嘗試設(shè)置它,將引發(fā)一個錯誤。
借助此方法,WinRT 異步操作的完成狀態(tài)和代表任務(wù)的完成狀態(tài)之間將建立起一對一的映射:
終端 AsyncStatus | 轉(zhuǎn)換為 TaskStatus | 等待內(nèi)容 |
Completed | RanToCompletion | 返回操作的結(jié)果(或 void) |
Error | Faulted | 拋出失敗操作的異常 |
Canceled | Canceled | 拋出 OperationCanceledException |
當(dāng)然,如果我們在每次希望等待某個 WinRT 異步操作時都必須編寫這段用于處理此 await 操作的樣板代碼,代碼很快將變得冗長而乏味。作為優(yōu)秀的程序開發(fā)人員,我們可以將該樣板封裝到一個方法中,以便反復(fù)使用。我們可以編寫一個將 WinRT 異步操作轉(zhuǎn)換為任務(wù)的擴展方法:
public static Task<TResult> AsTask<TResult>(
this IAsyncOperation<TResult> operation)
{
var tcs = new TaskCompletionSource<TResult>();
operation.Completed = delegate
{
switch(operation.Status)
{
AsyncStatus.Completed:
tcs.SetResult(operation.GetResults());
break;
AsyncStatus.Error:
tcs.SetException(operation.ErrorCode);
break;
AsyncStatus.Canceled:
tcs.SetCanceled();
break;
}
};
return tcs.Task;
}
借助該擴展方法,我現(xiàn)在可以編寫如下的代碼:
IAsyncOperation<string> op = SomeMethodAsync();
string result = await op.AsTask();
甚至可以簡化為:
string result = await SomeMethodAsync().AsTask();
這樣就好多了。當(dāng)然,此類可重用的 AsTask 功能對于在 C# 和 Visual Basic 中使用 WinRT 的任何人來說都必不可少,因此您實際上不需要編寫自己的實施:.NET 4.5 中已經(jīng)內(nèi)置了此類方法。System.Runtime.WindowsRuntime.dll 程序集包含了用于 WinRT 異步接口的這些擴展方法:
namespace System
{
public static class WindowsRuntimeSystemExtensions
{
// IAsyncAction
public static Task AsTask(
this IAsyncAction source);
public static Task AsTask(
this IAsyncAction source,
CancellationToken cancellationToken);
// IAsyncActionWithProgress
public static Task AsTask<TProgress>(
this IAsyncActionWithProgress<TProgress> source);
public static Task AsTask<TProgress>(
this IAsyncActionWithProgress<TProgress> source,
IProgress<TProgress> progress);
public static Task AsTask<TProgress>(
this IAsyncActionWithProgress<TProgress> source,
CancellationToken cancellationToken);
public static Task AsTask<TProgress>(
this IAsyncActionWithProgress<TProgress> source,
CancellationToken cancellationToken,
IProgress<TProgress> progress);
// IAsyncOperation
public static Task<TResult> AsTask<TResult>(
this IAsyncOperation<TResult> source);
public static Task<TResult> AsTask<TResult>(
this IAsyncOperation<TResult> source,
CancellationToken cancellationToken);
// IAsyncOperationWithProgress
public static Task<TResult> AsTask<TResult, TProgress>(
this IAsyncOperationWithProgress<TResult, TProgress> source);
public static Task<TResult> AsTask<TResult, TProgress>(
this IAsyncOperationWithProgress<TResult, TProgress> source,
IProgress<TProgress> progress);
public static Task<TResult> AsTask<TResult, TProgress>(
this IAsyncOperationWithProgress<TResult, TProgress> source,
CancellationToken cancellationToken);
public static Task<TResult> AsTask<TResult, TProgress>(
this IAsyncOperationWithProgress<TResult, TProgress> source,
CancellationToken cancellationToken,
IProgress<TProgress> progress);
...
}
}
這四種接口均具有一個無參數(shù)的 AsTask 重載,與我們剛剛從頭編寫那個非常類似。此外,每種接口還具有一個接受CancellationToken 的重載。此標(biāo)記是 .NET 中用于提供可組合和可合作取消的常見機制;您會向所有異步操作傳入一個標(biāo)記,而當(dāng)系統(tǒng)請求取消時,所有這些異步操作都將接到取消請求。您現(xiàn)在已經(jīng)知道存在現(xiàn)成可用的此類 API,但為了說明其原理,我們還是要向您介紹一下如何構(gòu)建此類 AsTask(CancellationToken) 重載。CancellationToken 提供了一個 Register 方法,該方法會接受將在請求取消時調(diào)用的委派;我們可以簡單地提供一個委派,然后通過取消請求調(diào)用 IAsyncInfo 對象中的 Cancel,并通過取消請求轉(zhuǎn)發(fā):
public static Task<TResult> AsTask<TResult>(
this IAsyncOperation<TResult> operation,
CancellationToken cancellationToken
{
using(cancellationToken.Register(() => operation.Cancel()))
return await operation.AsTask();
}
盡管 .NET 4.5 中自帶的實施并非與此完全相同,但邏輯上基本一致。
對于 IAsyncActionWithProgress<TProgress> 和 IAsyncOperationWithProgress<TResult,TProgress>,還具有接受IProgress<TProgress> 的重載。IProgress<T> 是一種可以由方法接受以通報回進度的 .NET 接口,而 AsTask 方法只需為 WinRT 異步操作的 Progress 屬性連接委派,即可將進度信息轉(zhuǎn)發(fā)給 IProgress。同樣,我們將展示手動實施該功能的方法,以供您進行參考:
public static Task<TResult> AsTask<TResult,TProgress>(
this IAsyncOperationWithProgress<TResult> operation,
IProgress<TProgress> progress
{
operation.Progress += (_,p) => progress.Report(p);
return operation.AsTask();
}
直接等待 WinRT 異步操作
我們現(xiàn)在已經(jīng)知道了如何創(chuàng)建代表 WinRT 異步操作的任務(wù),以便可以對其進行等待。但是,如何才能直接等待 WinRT 操作呢?換言之,最好能夠編寫如下的代碼:
await SomeMethodAsync().AsTask();
但對于不需要提供 CancellationToken 或 IProgress<T> 的情況,避免編寫調(diào)用 AsTask 的代碼豈不更好?
當(dāng)然,我們已在本博文的開頭提到,這是可以實現(xiàn)的。請回憶一下編譯器如何期待發(fā)現(xiàn)一個返回適當(dāng)?shù)却绦蝾愋偷?nbsp;GetAwaiter 方法。如前所述,System.Runtime.WindowsRuntime.dll 中的 WindowsRuntimeSystemExtensions 類型包含針對四種 WinRT 異步接口的此類 GetAwaiter 擴展方法:
namespace System
{
public static class WindowsRuntimeSystemExtensions
{
...
public static TaskAwaiter GetAwaiter(
this IAsyncAction source);
public static TaskAwaiter<TResult> GetAwaiter<TResult>(
this IAsyncOperation<TResult> source);
public static TaskAwaiter GetAwaiter<TProgress>(
this IAsyncActionWithProgress<TProgress> source);
public static TaskAwaiter<TResult> GetAwaiter<TResult, TProgress>(
this IAsyncOperationWithProgress<TResult, TProgress> source);
}
}
請注意來自這些方法的返回類型:TaskAwaiter 或 TaskAwaiter<TResult>。這些方法均利用了框架內(nèi)置的現(xiàn)有任務(wù)等待程序。根據(jù)已經(jīng)掌握的 AsTask 相關(guān)知識,您可能已經(jīng)猜到了這些功能的實施方法。框架中的實際實施基本上與此完全相同:
public static TaskAwaiter GetAwaiter(
this IAsyncAction source)
{
return source.AsTask().GetAwaiter();
}
這意味著這兩行代碼將引發(fā)完全相同的行為:
await SomeMethodAsync().AsTask();
await SomeMethodAsync();
自定義等待行為
如前所述,TaskAwaiter 和 TaskAwaiter<TResult> 可提供滿足編譯器對等待程序的期待所需的全部成員:
bool IsCompleted { get; }
void OnCompleted(Action continuation);
TResult GetResult(); //returns void on TaskAwaiter
其中最有趣的成員為 OnCompleted,它將在所等待的操作完成時,負責(zé)調(diào)用續(xù)體委派。OnCompleted 提供特殊封送行為,以確保續(xù)體委派在正確的位置執(zhí)行。
在默認情況下,當(dāng)任務(wù)等待程序的 OnCompleted 得到調(diào)用時,會通知當(dāng)前的 SynchronizationContext,SynchronizationContext 是代碼執(zhí)行環(huán)境的抽象表示。在 Metro 風(fēng)格應(yīng)用程序的 UI 線程中,SynchronizationContext.Current 會返回一個內(nèi)部 WinRTSynchronizationContext 類型的實例。SynchronizationContext 會提供一個虛擬的 Post 方法,該方法會接受一個委派,并在上下文的適當(dāng)位置執(zhí)行該委派;WinRTSynchronizationContext 封裝了一個 CoreDispatcher,并使用其 RunAsync 將委派異步調(diào)用回 UI 線程(我們稍早前已在本博文中手動實施了該功能)。當(dāng)所等待的任務(wù)完成時,委派將作為 Post 傳遞給 OnCompleted,以便在調(diào)用 OnCompleted時捕獲的當(dāng)前 SynchronizationContext 中執(zhí)行。正是該機制允許您在 UI 邏輯中使用 await 編寫代碼,而無需擔(dān)心是否能封送回正確的線程:任務(wù)的等待程序?qū)槟幚硗桩?dāng)。
當(dāng)然,在某些情況下,您可能不希望執(zhí)行這種默認的封送行為。此類情況多在庫中出現(xiàn):許多類型的庫不關(guān)心操作 UI 控件及運行其自身的線程,因此從性能的角度來考慮,避免與跨線程封送相關(guān)的開銷將不無裨益。為了適應(yīng)希望禁用這種默認封送行為的代碼,Task 和Task<TResult> 提供了 ConfigureAwait 方法。ConfigureAwait 接受布爾值 continueOnCapturedContext 參數(shù):傳遞 True 表示使用默認行為,傳遞 False 表示系統(tǒng)不需要將委派的調(diào)用強制封送回原始上下文,而是在系統(tǒng)認為適當(dāng)?shù)娜我馕恢脠?zhí)行該委派。
因此,如果您希望在不強迫剩余的代碼返回 UI 線程執(zhí)行的情況下等待某個 WinRT 操作,請編寫以下任一代碼作為替換:
或者:
await SomeMethodAsync().AsTask();
您可以編寫:
await SomeMethodAsync().AsTask()
.ConfigureAwait(continueOnCapturedContext:false);
或僅編寫:
await SomeMethodAsync().AsTask().ConfigureAwait(false);
何時使用 AsTask
如果您只是希望調(diào)用某個 WinRT 異步操作并等待其完成,直接等待該 WinRT 異步操作是最為簡單和清潔的方法:
但是如果您希望獲得更強的控制力,就需要使用 AsTask。我們已經(jīng)介紹了 AsTask 的一些用途:
- 通過 CancellationToken 支持取消
CancellationToken token = ...;
await SomeMethodAsync().AsTask(token);
- 通過 IProgress<T> 支持進度報告
IProgress<TProgress> progress = ...;
await SomeMethodAsync().AsTask(progress);
- 通過 ConfigureAwait 取消默認續(xù)體封送行為
await SomeMethodAsync().AsTask().ConfigureAwait(false);
在許多其他重要的情況下,AsTask 也能發(fā)揮很大的作用。
其中之一與 Task 支持多續(xù)體的功能有關(guān)。WinRT 異步操作類型僅支持注冊了 Completed 的單個委派(Completed 是一個屬性而非事件),并且該委派只能設(shè)置一次。在大多數(shù)情況下,這沒什么影響,因為您只需要等待該操作一次,例如,作為調(diào)用某個異步方法的替代方案:
您可以調(diào)用并等待某個異步對應(yīng)體:
邏輯上,這將保持與使用異步對應(yīng)體相同的控制流。但有時,您希望能夠掛接多個回調(diào),或者您希望能夠多次等待同一個實例。與 WinRT 異步接口相反,Task 類型支持任意次等待和/或任意次使用其 ContinueWith 方法以支持任意次回調(diào)。因此,您可以使用 AsTask 來為您的 WinRT 異步操作獲取任務(wù),然后將多個回調(diào)掛接到 Task 而不是直接掛接到 WinRT 異步操作。
var t = SomeMethodAsync().AsTask();
t.ContinueWith(delegate { ... });
t.ContinueWith(delegate { ... });
t.ContinueWith(delegate { ... });
AsTask 的另一個用途是處理通過 Task 或 Task<TResult> 類型運行的方法。Task.WhenAll 或 Task.WhenAny 等組合方法會通過 Task,而非 WinRT 異步接口運行。因此,如果您希望能夠調(diào)用多個 WinRT 異步操作,然后 await 他們?nèi)炕虿糠滞瓿桑梢允褂?nbsp;AsTask 來簡化這一過程。例如,本 await 會在所提供的三個???作中的任意一個完成時完成,并返回代表其內(nèi)容的 Task:
Task firstCompleted = await Task.WhenAny(
SomeMethod1Async().AsTask(),
SomeMethod2Async().AsTask(),
SomeMethod3Async().AsTask());
結(jié)論
了解到 WinRT 通過異步操作提供了如此眾多的功能的確令人倍感興奮;大量公開的此類 API 證明了響應(yīng)速度對于該平臺的重要性。這也意味著開發(fā)人員急切渴求用于處理這些操作的編程模型:對于 C# 和 Visual Basic 代碼,await 和 AsTask 可謂雪中送炭。希望本博文提供的技術(shù)內(nèi)幕能夠幫助您更好地理解這些功能的具體工作原理,并幫助您高效地開發(fā) Metro 風(fēng)格應(yīng)用程序。