使用以工作為基礎的非同步模式Consuming the Task-based Asynchronous Pattern

當您使用以工作為基礎的非同步模式 (TAP) 執行非同步作業時,您可以使用回撥來達到等待而不封鎖。When you use the Task-based Asynchronous Pattern (TAP) to work with asynchronous operations, you can use callbacks to achieve waiting without blocking. 就工作而言,這可透過 Task.ContinueWith 之類的方法來達成。For tasks, this is achieved through methods such as Task.ContinueWith. 以語言為基礎的非同步支援允許非同步作業在正常控制流程中等候,以隱藏回撥,而編譯器產生的程式碼提供這種相同的 API 層級支援。Language-based asynchronous support hides callbacks by allowing asynchronous operations to be awaited within normal control flow, and compiler-generated code provides this same API-level support.

使用 Await 暫停執行Suspending Execution with Await

從 .NET Framework 4.5 開始,您可以使用 C# 的 await 關鍵字和 Visual Basic 的 Await 運算子,以非同步方式等候 TaskTask<TResult> 物件。Starting with the .NET Framework 4.5, you can use the await keyword in C# and the Await Operator in Visual Basic to asynchronously await Task and Task<TResult> objects. 當您等候的是 Task 時,await 運算式的類型會是 voidWhen you're awaiting a Task, the await expression is of type void. 當您等候的是 Task<TResult> 時,await 運算式的類型會是 TResultWhen you're awaiting a Task<TResult>, the await expression is of type TResult. await 運算式必須發生在非同步方法的主體內。An await expression must occur inside the body of an asynchronous method. 如需 .NET Framework 4.5 中的 C# 和 Visual Basic 語言支援的詳細資訊,請參閱 C# 和 Visual Basic 語言規格。For more information about C# and Visual Basic language support in the .NET Framework 4.5, see the C# and Visual Basic language specifications.

實際上,等候功能會使用接續在工作上安裝回撥。Under the covers, the await functionality installs a callback on the task by using a continuation. 此回撥會從暫止點繼續非同步方法。This callback resumes the asynchronous method at the point of suspension. 當非同步方法繼續時,如果等候的作業順利完成,而且是 Task<TResult>,就會傳回其 TResultWhen the asynchronous method is resumed, if the awaited operation completed successfully and was a Task<TResult>, its TResult is returned. 如果所等候的 TaskTask<TResult>Canceled 狀態結束,則會擲回 OperationCanceledException 例外狀況。If the Task or Task<TResult> that was awaited ended in the Canceled state, an OperationCanceledException exception is thrown. 如果所等候的 TaskTask<TResult>Faulted 狀態結束,則會擲回造成它發生錯誤的例外狀況。If the Task or Task<TResult> that was awaited ended in the Faulted state, the exception that caused it to fault is thrown. Task 會因為多個例外狀況而錯誤,但只會傳播其中一個例外狀況。A Task can fault as a result of multiple exceptions, but only one of these exceptions is propagated. 不過,Task.Exception 屬性會傳回包含所有錯誤的 AggregateException 例外狀況。However, the Task.Exception property returns an AggregateException exception that contains all the errors.

如果同步處理內容 (SynchronizationContext 物件) 與暫止時正在執行非同步方法的執行緒關聯 (例如,如果 SynchronizationContext.Current 屬性不是 null),非同步方法就會在相同的同步處理內容上使用該內容的 Post 方法繼續執行。If a synchronization context (SynchronizationContext object) is associated with the thread that was executing the asynchronous method at the time of suspension (for example, if the SynchronizationContext.Current property is not null), the asynchronous method resumes on that same synchronization context by using the context’s Post method. 否則,它會依賴暫止時當前的工作排程器 (TaskScheduler 物件)。Otherwise, it relies on the task scheduler (TaskScheduler object) that was current at the time of suspension. 通常,這會是以執行緒集區為目標的預設工作排程器 (TaskScheduler.Default)。Typically, this is the default task scheduler (TaskScheduler.Default), which targets the thread pool. 這個工作排程器決定等候的非同步作業是否應該在完成處繼續,還是應該排定繼續。This task scheduler determines whether the awaited asynchronous operation should resume where it completed or whether the resumption should be scheduled. 預設排程器通常允許在完成等候作業的執行緒上接續執行。The default scheduler typically allows the continuation to run on the thread that the awaited operation completed.

呼叫非同步方法時會同步執行函式主體,直到尚未完成的可等候執行個體上的第一個 await 運算式為止,此時引動過程會返回呼叫端。When an asynchronous method is called, it synchronously executes the body of the function up until the first await expression on an awaitable instance that has not yet completed, at which point the invocation returns to the caller. 如果非同步方法並未傳回 void,則會傳回 TaskTask<TResult> 物件來代表進行中的計算。If the asynchronous method does not return void, a Task or Task<TResult> object is returned to represent the ongoing computation. 在非 void 非同步方法中,如果遇到 return 陳述式或到達方法主體的結尾,工作會以 RanToCompletion 最終狀態完成。In a non-void asynchronous method, if a return statement is encountered or the end of the method body is reached, the task is completed in the RanToCompletion final state. 如果未處理的例外狀況導致控制權離開非同步方法的主體,工作則會以 Faulted 狀態結束。If an unhandled exception causes control to leave the body of the asynchronous method, the task ends in the Faulted state. 如果例外狀況是 OperationCanceledException,工作就會變成在 Canceled 狀態下結束。If that exception is an OperationCanceledException, the task instead ends in the Canceled state. 就這樣,最後會發佈結果或例外狀況。In this manner, the result or exception is eventually published.

此行為有數個重要的變化。There are several important variations of this behavior. 基於效能考量,如果工作在開始等候之前已完成,則不會讓出控制權,函式會繼續執行。For performance reasons, if a task has already completed by the time the task is awaited, control is not yielded, and the function continues to execute. 此外,返回原始內容不一見得是想要的行為,可以變更;下一節會詳細說明。Additionally, returning to the original context isn't always the desired behavior and can be changed; this is described in more detail in the next section.

使用 Yield 和 ConfigureAwait 設定暫止和繼續Configuring Suspension and Resumption with Yield and ConfigureAwait

有數種方法可以更充分掌控非同步方法的執行。Several methods provide more control over an asynchronous method’s execution. 例如,您可以使用 Task.Yield 方法在非同步方法的中導入 Yield 點:For example, you can use the Task.Yield method to introduce a yield point into the asynchronous method:

public class Task : …
{
    public static YieldAwaitable Yield();
    …
}

這相當於以非同步方式往回張貼或排定到目前的內容。This is equivalent to asynchronously posting or scheduling back to the current context.

Task.Run(async delegate
{
    for(int i=0; i<1000000; i++)
    {
        await Task.Yield(); // fork the continuation into a separate work item
        ...
    }
});

您也可以使用 Task.ConfigureAwait 方法,更充分掌控非同步方法中的暫止和繼續。You can also use the Task.ConfigureAwait method for better control over suspension and resumption in an asynchronous method. 如前面所述,根據預設,目前的內容是在非同步方法暫停時擷取,並在恢復時用該擷取的內容來叫用非同步方法的接續動作。As mentioned previously, by default, the current context is captured at the time an asynchronous method is suspended, and that captured context is used to invoke the asynchronous method’s continuation upon resumption. 在許多情況下,這就是您想要的行為。In many cases, this is the exact behavior you want. 在其他情況下,您可能不在意接續內容,您可以避免這樣往回張貼到原始內容,以達到更高效能。In other cases, you may not care about the continuation context, and you can achieve better performance by avoiding such posts back to the original context. 若要這樣做,請使用 Task.ConfigureAwait 方法來告知等候作業不要在內容上擷取和繼續,而是只要所等候的非同步作業完成,就繼續執行︰To enable this, use the Task.ConfigureAwait method to inform the await operation not to capture and resume on the context, but to continue execution wherever the asynchronous operation that was being awaited completed:

await someTask.ConfigureAwait(continueOnCapturedContext:false);

取消非同步作業Canceling an Asynchronous Operation

從 .NET Framework 4 開始,支援取消的 TAP 方法即至少提供一個可接受取消權杖 (CancellationToken 物件) 的多載。Starting with the .NET Framework 4, TAP methods that support cancellation provide at least one overload that accepts a cancellation token (CancellationToken object).

建立取消權杖時,會透過某個取消權杖來源 (CancellationTokenSource 物件) 來建立。A cancellation token is created through a cancellation token source (CancellationTokenSource object). 來源的 Token 屬性會傳回取消權杖,當呼叫來源的 Cancel 方法時,此權杖就會收到訊號。The source’s Token property returns the cancellation token that will be signaled when the source’s Cancel method is called. 例如,若要下載單一網頁,而且想要能夠取消作業,則需建立 CancellationTokenSource 物件,將其權杖傳遞給 TAP 方法,然後在您準備好要取消作業時,呼叫來源的 Cancel 方法︰For example, if you want to download a single webpage and you want to be able to cancel the operation, you create a CancellationTokenSource object, pass its token to the TAP method, and then call the source’s Cancel method when you're ready to cancel the operation:

var cts = new CancellationTokenSource();
string result = await DownloadStringAsync(url, cts.Token);
… // at some point later, potentially on another thread
cts.Cancel();

若要取消多個非同步引動過程,您可以將相同的語彙基元傳遞至所有引動過程︰To cancel multiple asynchronous invocations, you can pass the same token to all invocations:

var cts = new CancellationTokenSource();
    IList<string> results = await Task.WhenAll(from url in urls select DownloadStringAsync(url, cts.Token));
    // at some point later, potentially on another thread
    …
    cts.Cancel();

或者,您可以將相同的語彙基元傳遞至一組經過挑選的作業︰Or, you can pass the same token to a selective subset of operations:

var cts = new CancellationTokenSource();
    byte [] data = await DownloadDataAsync(url, cts.Token);
    await SaveToDiskAsync(outputPath, data, CancellationToken.None);
    … // at some point later, potentially on another thread
    cts.Cancel();

任何執行緒都可能起始取消要求。Cancellation requests may be initiated from any thread.

您可以將 CancellationToken.None 值傳遞給任何接受取消權杖的方法,以表示永遠不要求取消作業。You can pass the CancellationToken.None value to any method that accepts a cancellation token to indicate that cancellation will never be requested. 這會導致 CancellationToken.CanBeCanceled 屬性傳回 false,而可相應地將所呼叫的方法最佳化。This causes the CancellationToken.CanBeCanceled property to return false, and the called method can optimize accordingly. 在測試時,您也可以傳入以建構函式具現化之預先取消的取消語彙基元,此建構函式接受布林值,以指出語彙基元最初應該處於已取消或不可取消狀態。For testing purposes, you can also pass in a pre-canceled cancellation token that is instantiated by using the constructor that accepts a Boolean value to indicate whether the token should start in an already-canceled or not-cancelable state.

這種取消方法有幾項優點︰This approach to cancellation has several advantages:

  • 您可以將相同的取消語彙基元傳遞至任何數目的非同步和同步作業。You can pass the same cancellation token to any number of asynchronous and synchronous operations.

  • 相同的取消要求可以擴散至任何數目的接聽程式。The same cancellation request may be proliferated to any number of listeners.

  • 非同步 API 的開發人員可以完全控制是否可要求取消及何時生效。The developer of the asynchronous API is in complete control of whether cancellation may be requested and when it may take effect.

  • 使用 API 的程式碼可能選擇性地決定要將取消要求傳播至其中的非同步引動過程。The code that consumes the API may selectively determine the asynchronous invocations that cancellation requests will be propagated to.

監視進度Monitoring Progress

某些非同步方法會透過傳遞至非同步方法的進度介面來公開進度。Some asynchronous methods expose progress through a progress interface passed into the asynchronous method. 例如,假設一個函式以非同步方式下載文字字串,對於過程中以進度更新顯示目前為止完成的下載百分比。For example, consider a function which asynchronously downloads a string of text, and along the way raises progress updates that include the percentage of the download that has completed thus far. Windows Presentation Foundation (WPF) 應用程式中可以使用這種方法,如下所示︰Such a method could be consumed in a Windows Presentation Foundation (WPF) application as follows:

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; }
}

使用內建以工作為基礎的組合器Using the Built-in Task-based Combinators

System.Threading.Tasks 命名空間包含數個用於撰寫和處理工作的方式。The System.Threading.Tasks namespace includes several methods for composing and working with tasks.

Task.RunTask.Run

Task類別包含數個 Run 方法,可讓您輕鬆地將工作以 TaskTask<TResult> 的形式卸載至執行緒集區,例如:The Task class includes several Run methods that let you easily offload work as a Task or Task<TResult> to the thread pool, for example:

public async void button1_Click(object sender, EventArgs e)
{
    textBox1.Text = await Task.Run(() =>
    {
        // … do compute-bound work here
        return answer;
    });
}

這些 Run 方法中有些 (例如 Task.Run(Func<Task>) 多載) 會作為 TaskFactory.StartNew 方法的速記。Some of these Run methods, such as the Task.Run(Func<Task>) overload, exist as shorthand for the TaskFactory.StartNew method. 其他多載 (例如 Task.Run(Func<Task>)) 則可讓您在所卸載的工作中使用 await,例如:Other overloads, such as Task.Run(Func<Task>), enable you to use await within the offloaded work, for example:

public async void button1_Click(object sender, EventArgs e)
{
    pictureBox1.Image = await Task.Run(async() =>
    {
        using(Bitmap bmp1 = await DownloadFirstImageAsync())
        using(Bitmap bmp2 = await DownloadSecondImageAsync())
        return Mashup(bmp1, bmp2);
    });
}

這類多載在邏輯上等同於使用 TaskFactory.StartNew 方法搭配「工作平行程式庫」中的 Unwrap 擴充方法。Such overloads are logically equivalent to using the TaskFactory.StartNew method in conjunction with the Unwrap extension method in the Task Parallel Library.

Task.FromResultTask.FromResult

在可能已有資料可用,而只需從放在 Task<TResult> 中的工作傳回方法傳回的情況下,請使用 FromResult 方法:Use the FromResult method in scenarios where data may already be available and just needs to be returned from a task-returning method lifted into a Task<TResult>:

public Task<int> GetValueAsync(string key)
{
    int cachedValue;
    return TryGetCachedValue(out cachedValue) ?
        Task.FromResult(cachedValue) :
        GetValueAsyncInternal();
}

private async Task<int> GetValueAsyncInternal(string key)
{
    …
}

Task.WhenAllTask.WhenAll

請使用 WhenAll 方法,以非同步方式等候多個以工作表示的非同步作業。Use the WhenAll method to asynchronously wait on multiple asynchronous operations that are represented as tasks. 此方法有多個多載可支援一組非泛型工作或一組非統一泛型工作 (例如,非同步等候多個 void 傳回作業,或非同步等候多個值傳回方法,而每個值可能有不同型別),也支援一組統一泛型工作 (例如,非同步等候多個 TResult 傳回方法)。The method has multiple overloads that support a set of non-generic tasks or a non-uniform set of generic tasks (for example, asynchronously waiting for multiple void-returning operations, or asynchronously waiting for multiple value-returning methods where each value may have a different type) and to support a uniform set of generic tasks (such as asynchronously waiting for multiple TResult-returning methods).

假設您想要將電子郵件訊息傳送給數個客戶。Let's say you want to send email messages to several customers. 您可以重疊傳送訊息,不必等待一個訊息完成才傳送下一個訊息。You can overlap sending the messages so you're not waiting for one message to complete before sending the next. 您也可以查明傳送作業何時完成及是否發生任何錯誤︰You can also find out when the send operations have completed and whether any errors have occurred:

IEnumerable<Task> asyncOps = from addr in addrs select SendMailAsync(addr);
await Task.WhenAll(asyncOps);

以下程式碼不會明確處理可能發生的例外狀況,而是會在 WhenAll 產生的工作上,讓例外狀況從 await 傳播出來。This code doesn't explicitly handle exceptions that may occur, but lets exceptions propagate out of the await on the resulting task from WhenAll. 若要處理例外狀況,您可以使用下列程式碼︰To handle the exceptions, you can use code such as the following:

IEnumerable<Task> asyncOps = from addr in addrs select SendMailAsync(addr);
try
{
    await Task.WhenAll(asyncOps);
}
catch(Exception exc)
{
    ...
}

在此案例中,如果任何非同步作業失敗,所有例外狀況會合併成一個 AggregateException 例外狀況,並儲存在 WhenAll 方法所傳回的 Task 中。In this case, if any asynchronous operation fails, all the exceptions will be consolidated in an AggregateException exception, which is stored in the Task that is returned from the WhenAll method. 不過,await 關鍵字只會傳播其中一個例外狀況。However, only one of those exceptions is propagated by the await keyword. 如果您想要檢查所有例外狀況,您可以改寫先前的程式碼,如下所示︰If you want to examine all the exceptions, you can rewrite the previous code as follows:

Task [] asyncOps = (from addr in addrs select SendMailAsync(addr)).ToArray();
try
{
    await Task.WhenAll(asyncOps);
}
catch(Exception exc)
{
    foreach(Task faulted in asyncOps.Where(t => t.IsFaulted))
    {
        … // work with faulted and faulted.Exception
    }
}

舉例說明,假設從 web 非同步下載多個檔案。Let's consider an example of downloading multiple files from the web asynchronously. 在此例子中,所有非同步作業有相同的結果型別,很容易存取結果︰In this case, all the asynchronous operations have homogeneous result types, and it's easy to access the results:

string [] pages = await Task.WhenAll(
    from url in urls select DownloadStringAsync(url));

您可以使用我們在先前 void 傳回案例中討論過的相同例外狀況處理技術︰You can use the same exception-handling techniques we discussed in the previous void-returning scenario:

Task [] asyncOps =
    (from url in urls select DownloadStringAsync(url)).ToArray();
try
{
    string [] pages = await Task.WhenAll(asyncOps);
    ...
}
catch(Exception exc)
{
    foreach(Task<string> faulted in asyncOps.Where(t => t.IsFaulted))
    {
        … // work with faulted and faulted.Exception
    }
}

Task.WhenAnyTask.WhenAny

您可以使用 WhenAny 方法,在多個以工作表示的非同步作業中,只以非同步方式等候其中一個完成。You can use the WhenAny method to asynchronously wait for just one of multiple asynchronous operations represented as tasks to complete. 這個方法適用於四種主要的使用案例:This method serves four primary use cases:

  • 備援:多次執行某項作業並選擇最先完成的作業 (例如,連絡會產生單一結果之提供股價報價服務的多個網站,且選擇最快完成的那一個)。Redundancy: Performing an operation multiple times and selecting the one that completes first (for example, contacting multiple stock quote web services that will produce a single result and selecting the one that completes the fastest).

  • 交錯:啟動多項作業並等候所有作業完成,不過,會在作業完成時才進行處理。Interleaving: Launching multiple operations and waiting for all of them to complete, but processing them as they complete.

  • 節流:當作業完成時,允許其他作業開始。Throttling: Allowing additional operations to begin as others complete. 這是交錯情節的擴充。This is an extension of the interleaving scenario.

  • 提早釋出:例如,工作 t1 代表的作業可以在 WhenAny 工作中與另一個工作 t2 合併成群組,然後您可以等候 WhenAny 工作。Early bailout: For example, an operation represented by task t1 can be grouped in a WhenAny task with another task t2, and you can wait on the WhenAny task. 工作 t2 可代表造成 WhenAny 工作比 t1 更早完成的逾時、取消或一些其他訊號。Task t2 could represent a time-out, or cancellation, or some other signal that causes the WhenAny task to complete before t1 completes.

備援性Redundancy

假設您想決定是否購買股票。Consider a case where you want to make a decision about whether to buy a stock. 您有好幾個信任的股票建議 Web 服務,不過依據每日負載,每個服務可能會在不同時間點變得很慢。There are several stock recommendation web services that you trust, but depending on daily load, each service can end up being slow at different times. 您可以使用 WhenAny 方法來接收任何作業完成的通知:You can use the WhenAny method to receive a notification when any operation completes:

var recommendations = new List<Task<bool>>()
{
    GetBuyRecommendation1Async(symbol),
    GetBuyRecommendation2Async(symbol),
    GetBuyRecommendation3Async(symbol)
};
Task<bool> recommendation = await Task.WhenAny(recommendations);
if (await recommendation) BuyStock(symbol);

不同於 WhenAll 會傳回所有成功完成之工作的未包裝結果,WhenAny 會傳回已完成的工作。Unlike WhenAll, which returns the unwrapped results of all tasks that completed successfully, WhenAny returns the task that completed. 如果工作失敗,必須知道它已失敗,如果工作成功,必須知道與傳回值相關聯的工作。If a task fails, it’s important to know that it failed, and if a task succeeds, it’s important to know which task the return value is associated with. 因此,您需要存取所傳回工作的結果,或進一步等待,如這個範例所示。Therefore, you need to access the result of the returned task, or further await it, as this example shows.

與使用 WhenAll 時相同,您必須能夠通融例外狀況。As with WhenAll, you have to be able to accommodate exceptions. 因為您要收回已完成的工作,您可以等候傳回的工作傳播錯誤,並適當地 try/catch,例如︰Because you receive the completed task back, you can await the returned task to have errors propagated, and try/catch them appropriately; for example:

Task<bool> [] recommendations = …;
while(recommendations.Count > 0)
{
    Task<bool> recommendation = await Task.WhenAny(recommendations);
    try
    {
        if (await recommendation) BuyStock(symbol);
        break;
    }
    catch(WebException exc)
    {
        recommendations.Remove(recommendation);
    }
}

此外,即使第一個工作成功完成,後續工作可能失敗。Additionally, even if a first task completes successfully, subsequent tasks may fail. 此時,您有幾個處理例外狀況的選項:您可以等待所有啟動的工作完成,在這種情況下,您可以使用 WhenAll 方法,或者您可以決定所有例外狀況都很重要且必須全部記錄。At this point, you have several options for dealing with exceptions: You can wait until all the launched tasks have completed, in which case you can use the WhenAll method, or you can decide that all exceptions are important and must be logged. 對此,您可以使用接續,在工作完成時以非同步方式接收通知︰For this, you can use continuations to receive a notification when tasks have completed asynchronously:

foreach(Task recommendation in recommendations)
{
    var ignored = recommendation.ContinueWith(
        t => { if (t.IsFaulted) Log(t.Exception); });
}

或:or:

foreach(Task recommendation in recommendations)
{
    var ignored = recommendation.ContinueWith(
        t => Log(t.Exception), TaskContinuationOptions.OnlyOnFaulted);
}

或甚至︰or even:

private static async void LogCompletionIfFailed(IEnumerable<Task> tasks)
{
    foreach(var task in tasks)
    {
        try { await task; }
        catch(Exception exc) { Log(exc); }
    }
}
…
LogCompletionIfFailed(recommendations);

最後,您可能想要取消其餘所有作業︰Finally, you may want to cancel all the remaining operations:

var cts = new CancellationTokenSource();
var recommendations = new List<Task<bool>>()
{
    GetBuyRecommendation1Async(symbol, cts.Token),
    GetBuyRecommendation2Async(symbol, cts.Token),
    GetBuyRecommendation3Async(symbol, cts.Token)
};

Task<bool> recommendation = await Task.WhenAny(recommendations);
cts.Cancel();
if (await recommendation) BuyStock(symbol);

交錯Interleaving

假設您從 web 下載影像並處理每個影像 (例如,將影像新增至 UI 控制項)。Consider a case where you're downloading images from the web and processing each image (for example, adding the image to a UI control). 您必須在 UI 執行緒上循序處理,但您想要盡可能同時下載影像。You have to do the processing sequentially on the UI thread, but you want to download the images as concurrently as possible. 此外,您不想等到影像全部下載後才新增至 UI,您想要在影像完成下載時就新增:Also, you don’t want to hold up adding the images to the UI until they’re all downloaded—you want to add them as they complete:

List<Task<Bitmap>> imageTasks =
    (from imageUrl in urls select GetBitmapAsync(imageUrl)).ToList();
while(imageTasks.Count > 0)
{
    try
    {
        Task<Bitmap> imageTask = await Task.WhenAny(imageTasks);
        imageTasks.Remove(imageTask);

        Bitmap image = await imageTask;
        panel.AddImage(image);
    }
    catch{}
}

如果所下載影像的 ThreadPool 上涉及密集運算處理,您也可以套用交錯,例如:You can also apply interleaving to a scenario that involves computationally intensive processing on the ThreadPool of the downloaded images; for example:

List<Task<Bitmap>> imageTasks =
    (from imageUrl in urls select GetBitmapAsync(imageUrl)
         .ContinueWith(t => ConvertImage(t.Result)).ToList();
while(imageTasks.Count > 0)
{
    try
    {
        Task<Bitmap> imageTask = await Task.WhenAny(imageTasks);
        imageTasks.Remove(imageTask);

        Bitmap image = await imageTask;
        panel.AddImage(image);
    }
    catch{}
}

節流Throttling

在交錯範例中,除非使用者下載太多影像,以至於必須節流下載。例如,您只想要同時進行一定數量的下載。Consider the interleaving example, except that the user is downloading so many images that the downloads have to be throttled; for example, you want only a specific number of downloads to happen concurrently. 為了達到此目的,您可以先啟動一部分的非同步作業。To achieve this, you can start a subset of the asynchronous operations. 當這些作業完成時,您可以再啟動另一批作業︰As operations complete, you can start additional operations to take their place:

const int CONCURRENCY_LEVEL = 15;
Uri [] urls = …;
int nextIndex = 0;
var imageTasks = new List<Task<Bitmap>>();
while(nextIndex < CONCURRENCY_LEVEL && nextIndex < urls.Length)
{
    imageTasks.Add(GetBitmapAsync(urls[nextIndex]));
    nextIndex++;
}

while(imageTasks.Count > 0)
{
    try
    {
        Task<Bitmap> imageTask = await Task.WhenAny(imageTasks);
        imageTasks.Remove(imageTask);

        Bitmap image = await imageTask;
        panel.AddImage(image);
    }
    catch(Exception exc) { Log(exc); }

    if (nextIndex < urls.Length)
    {
        imageTasks.Add(GetBitmapAsync(urls[nextIndex]));
        nextIndex++;
    }
}

提早脫離Early Bailout

假設您以非同步方式等候作業完成,同時也回應使用者的取消要求 (例如,使用者按一下取消按鈕)。Consider that you're waiting asynchronously for an operation to complete while simultaneously responding to a user’s cancellation request (for example, the user clicked a cancel button). 下列程式碼說明這種情節:The following code illustrates this scenario:

private CancellationTokenSource m_cts;

public void btnCancel_Click(object sender, EventArgs e)
{
    if (m_cts != null) m_cts.Cancel();
}

public async void btnRun_Click(object sender, EventArgs e)
{
    m_cts = new CancellationTokenSource();
    btnRun.Enabled = false;
    try
    {
        Task<Bitmap> imageDownload = GetBitmapAsync(txtUrl.Text);
        await UntilCompletionOrCancellation(imageDownload, m_cts.Token);
        if (imageDownload.IsCompleted)
        {
            Bitmap image = await imageDownload;
            panel.AddImage(image);
        }
        else imageDownload.ContinueWith(t => Log(t));
    }
    finally { btnRun.Enabled = true; }
}

private static async Task UntilCompletionOrCancellation(
    Task asyncOp, CancellationToken ct)
{
    var tcs = new TaskCompletionSource<bool>();
    using(ct.Register(() => tcs.TrySetResult(true)))
        await Task.WhenAny(asyncOp, tcs.Task);
    return asyncOp;
}

當您決定脫離時,此實作會立即重新啟用使用者介面,但不取消幕後的非同步作業。This implementation re-enables the user interface as soon as you decide to bail out, but doesn't cancel the underlying asynchronous operations. 另一種方式是在您決定脫離時取消暫止的作業,但在作業實際完成之前不重新建立使用者介面,可能是由於取消要求而提早結束︰Another alternative would be to cancel the pending operations when you decide to bail out, but not reestablish the user interface until the operations actually complete, potentially due to ending early due to the cancellation request:

private CancellationTokenSource m_cts;

public async void btnRun_Click(object sender, EventArgs e)
{
    m_cts = new CancellationTokenSource();

    btnRun.Enabled = false;
    try
    {
        Task<Bitmap> imageDownload = GetBitmapAsync(txtUrl.Text, m_cts.Token);
        await UntilCompletionOrCancellation(imageDownload, m_cts.Token);
        Bitmap image = await imageDownload;
        panel.AddImage(image);
    }
    catch(OperationCanceledException) {}
    finally { btnRun.Enabled = true; }
}

另一個提早釋出的例子涉及使用 WhenAny 方法搭配 Delay 方法,如下一節所述。Another example of early bailout involves using the WhenAny method in conjunction with the Delay method, as discussed in the next section.

Task.DelayTask.Delay

您可以使用 Task.Delay 方法在非同步方法的執行過程中引入暫停。You can use the Task.Delay method to introduce pauses into an asynchronous method’s execution. 這適用於許多種功能,包括建置輪詢迴圈和將使用者輸入延遲一段預先定義的時間再處理。This is useful for many kinds of functionality, including building polling loops and delaying the handling of user input for a predetermined period of time. Task.DelayTask.WhenAny 搭配使用時也相當實用,可用來在等候上實作逾時。The Task.Delay method can also be useful in combination with Task.WhenAny for implementing time-outs on awaits.

如果更大非同步作業 (例如,ASP.NET Web 服務) 中的一項工作太久才完成,將會波及整體作業,尤其是如果它根本無法完成。If a task that’s part of a larger asynchronous operation (for example, an ASP.NET web service) takes too long to complete, the overall operation could suffer, especially if it fails to ever complete. 因此,在非同步作業上等候時必須能夠逾時。For this reason, it’s important to be able to time out when waiting on an asynchronous operation. 同步的 Task.WaitTask.WaitAllTask.WaitAny 方法可接受逾時值,但對應的 TaskFactory.ContinueWhenAll/Task.WhenAny 和先前提到的 Task.WhenAll/Task.WhenAny 方法則無法接受逾時值。The synchronous Task.Wait, Task.WaitAll, and Task.WaitAny methods accept time-out values, but the corresponding TaskFactory.ContinueWhenAll/Task.WhenAny and the previously mentioned Task.WhenAll/Task.WhenAny methods do not. 您可以改用 Task.Delay 搭配 Task.WhenAny 來實作逾時。Instead, you can use Task.Delay and Task.WhenAny in combination to implement a time-out.

例如,在您的應用程式 UI 中,假設您想要下載影像,並於下載影像時停用 UI。For example, in your UI application, let's say that you want to download an image and disable the UI while the image is downloading. 不過,如果下載的時間太長,您想要重新啟用 UI 並放棄下載︰However, if the download takes too long, you want to re-enable the UI and discard the download:

public async void btnDownload_Click(object sender, EventArgs e)
{
    btnDownload.Enabled = false;
    try
    {
        Task<Bitmap> download = GetBitmapAsync(url);
        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; }
}

這同樣適用於多個下載,因為 WhenAll 會傳回工作:The same applies to multiple downloads, because WhenAll returns a task:

public async void btnDownload_Click(object sender, RoutedEventArgs e)
{
    btnDownload.Enabled = false;
    try
    {
        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));
        }
    }
    finally { btnDownload.Enabled = true; }
}

建置以工作為基礎的組合器Building Task-based Combinators

因為工作能夠完全代表非同步作業,並提供同步和非同步功能來聯結作業、擷取其結果等等,所以您可以建置實用的組合器程式庫,將工作組合以建置更大的模式。Because a task is able to completely represent an asynchronous operation and provide synchronous and asynchronous capabilities for joining with the operation, retrieving its results, and so on, you can build useful libraries of combinators that compose tasks to build larger patterns. 如上一節所述,.NET Framework 包含數個內建的組合器,但您也可以建立自己的組合器。As discussed in the previous section, the .NET Framework includes several built-in combinators, but you can also build your own. 下列章節提供幾個範例說明可能的結合器方法和型別。The following sections provide several examples of potential combinator methods and types.

RetryOnFaultRetryOnFault

在許多情況下,您可能想要在作業前次嘗試失敗時重試。In many situations, you may want to retry an operation if a previous attempt fails. 在同步程式碼中,您可能建置協助程式方法來達到這個目的,例如下列範例中的 RetryOnFaultFor synchronous code, you might build a helper method such as RetryOnFault in the following example to accomplish this:

public static T RetryOnFault<T>(
    Func<T> function, int maxTries)
{
    for(int i=0; i<maxTries; i++)
    {
        try { return function(); }
        catch { if (i == maxTries-1) throw; }
    }
    return default(T);
}

對於以 TAP 實作而傳回工作的非同步作業,您可以建置幾乎完全相同的協助程式方法:You can build an almost identical helper method for asynchronous operations that are implemented with TAP and thus return tasks:

public static async Task<T> RetryOnFault<T>(
    Func<Task<T>> function, int maxTries)
{
    for(int i=0; i<maxTries; i++)
    {
        try { return await function().ConfigureAwait(false); }
        catch { if (i == maxTries-1) throw; }
    }
    return default(T);
}

然後,您可以利用此組合器,將重試編碼到應用程式的邏輯中,例如︰You can then use this combinator to encode retries into the application’s logic; for example:

// Download the URL, trying up to three times in case of failure
string pageContents = await RetryOnFault(
    () => DownloadStringAsync(url), 3);

您可以進一步擴充 RetryOnFault 函式。You could extend the RetryOnFault function further. 例如,此函式可以接受另一個 Func<Task> 在重試之間叫用,以判斷何時再次嘗試,例如︰For example, the function could accept another Func<Task> that will be invoked between retries to determine when to try the operation again; for example:

public static async Task<T> RetryOnFault<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; }
        await retryWhen().ConfigureAwait(false);
    }
    return default(T);
}

然後,您可以如下所示使用此函式,先等待一秒再重試作業︰You could then use the function as follows to wait for a second before retrying the operation:

// Download the URL, trying up to three times in case of failure,
// and delaying for a second between retries
string pageContents = await RetryOnFault(
    () => DownloadStringAsync(url), 3, () => Task.Delay(1000));

NeedOnlyOneNeedOnlyOne

有時候,您可以利用備援性來改善作業的延遲,提高成功機會。Sometimes, you can take advantage of redundancy to improve an operation’s latency and chances for success. 假設有多個 Web 服務在一天中的不同時間提供股票報價,每個服務可能提供不同的品質等級和回應時間。Consider multiple web services that provide stock quotes, but at various times of the day, each service may provide different levels of quality and response times. 為了因應這些變動,您可以發出要求給所有 Web 服務,只要從其中一個服務收到回應,就立刻取消其餘要求。To deal with these fluctuations, you may issue requests to all the web services, and as soon as you get a response from one, cancel the remaining requests. 您可以實作協助程式函式,更輕鬆地實作這種常見模式,亦即啟動多項作業、等候任何回應,然後取消其餘作業。You can implement a helper function to make it easier to implement this common pattern of launching multiple operations, waiting for any, and then canceling the rest. 下列範例中的 NeedOnlyOne 函式說明此情節:The NeedOnlyOne function in the following example illustrates this scenario:

public static async Task<T> NeedOnlyOne(
    params Func<CancellationToken,Task<T>> [] functions)
{
    var cts = new CancellationTokenSource();
    var tasks = (from function in functions
                 select function(cts.Token)).ToArray();
    var completed = await Task.WhenAny(tasks).ConfigureAwait(false);
    cts.Cancel();
    foreach(var task in tasks)
    {
        var ignored = task.ContinueWith(
            t => Log(t), TaskContinuationOptions.OnlyOnFaulted);
    }
    return completed;
}

然後,您可以如下所示使用此函式︰You can then use this function as follows:

double currentPrice = await NeedOnlyOne(
    ct => GetCurrentPriceFromServer1Async("msft", ct),
    ct => GetCurrentPriceFromServer2Async("msft", ct),
    ct => GetCurrentPriceFromServer3Async("msft", ct));

交錯作業Interleaved Operations

當您處理非常大量的工作時,使用 WhenAny 方法來支援交錯案例會有潛在的效能問題。There is a potential performance problem with using the WhenAny method to support an interleaving scenario when you're working with very large sets of tasks. WhenAny 發出的每個呼叫會導致向每個工作登錄接續。Every call to WhenAny results in a continuation being registered with each task. 以 N 個工作為例,這會導致在交錯作業的存留期建立 O(N2) 個接續。For N number of tasks, this results in O(N2) continuations created over the lifetime of the interleaving operation. 如果您要處理的工作量很大,可以使用組合器 (如下列範例中的 Interleaved) 來解決效能問題:If you're working with a large set of tasks, you can use a combinator (Interleaved in the following example) to address the performance issue:

static IEnumerable<Task<T>> Interleaved<T>(IEnumerable<Task<T>> tasks)
{
    var inputTasks = tasks.ToList();
    var sources = (from _ in Enumerable.Range(0, inputTasks.Count)
                   select new TaskCompletionSource<T>()).ToList();
    int nextTaskIndex = -1;
    foreach (var inputTask in inputTasks)
    {
        inputTask.ContinueWith(completed =>
        {
            var source = sources[Interlocked.Increment(ref nextTaskIndex)];
            if (completed.IsFaulted)
                source.TrySetException(completed.Exception.InnerExceptions);
            else if (completed.IsCanceled)
                source.TrySetCanceled();
            else
                source.TrySetResult(completed.Result);
        }, CancellationToken.None,
           TaskContinuationOptions.ExecuteSynchronously,
           TaskScheduler.Default);
    }
    return from source in sources
           select source.Task;
}

然後,您可以利用組合器處理工作完成時的結果,例如︰You can then use the combinator to process the results of tasks as they complete; for example:

IEnumerable<Task<int>> tasks = ...;
foreach(var task in Interleaved(tasks))
{
    int result = await task;
    …
}

WhenAllOrFirstExceptionWhenAllOrFirstException

在某些分散/集中的情節中,您可能想要等候一個集合的所有工作,但如果其中一個發生錯誤,您想要在發生例外狀況時立刻停止等候。In certain scatter/gather scenarios, you might want to wait for all tasks in a set, unless one of them faults, in which case you want to stop waiting as soon as the exception occurs. 您可以使用結合器方法來達到此目的,如下列範例中的 WhenAllOrFirstExceptionYou can accomplish that with a combinator method such as WhenAllOrFirstException in the following example:

public static Task<T[]> WhenAllOrFirstException<T>(IEnumerable<Task<T>> tasks)
{
    var inputs = tasks.ToList();
    var ce = new CountdownEvent(inputs.Count);
    var tcs = new TaskCompletionSource<T[]>();

    Action<Task> onCompleted = (Task completed) =>
    {
        if (completed.IsFaulted)
            tcs.TrySetException(completed.Exception.InnerExceptions);
        if (ce.Signal() && !tcs.Task.IsCompleted)
            tcs.TrySetResult(inputs.Select(t => t.Result).ToArray());
    };

    foreach (var t in inputs) t.ContinueWith(onCompleted);
    return tcs.Task;
}

建置以工作為基礎的資料結構Building Task-based Data Structures

除了建置以工作為基礎之自訂組合器的能力之外,在 TaskTask<TResult> 中提供資料結構來同時代表非同步作業的結果及與其聯結時所需的同步處理,還可讓它變成非常強大的類型,在此類型上即可建置用於非同步案例中的自訂資料結構。In addition to the ability to build custom task-based combinators, having a data structure in Task and Task<TResult> that represents both the results of an asynchronous operation and the necessary synchronization to join with it makes it a very powerful type on which to build custom data structures to be used in asynchronous scenarios.

AsyncCacheAsyncCache

工作的其中一個重要層面就是它可以交給多個取用者,這些取用者全都可以等待它、向它登錄接續、取得其結果或例外狀況 (如果是 Task<TResult>)等。One important aspect of a task is that it may be handed out to multiple consumers, all of whom may await it, register continuations with it, get its result or exceptions (in the case of Task<TResult>), and so on. 這使得 TaskTask<TResult> 非常適合用於非同步快取基礎結構中。This makes Task and Task<TResult> perfectly suited to be used in an asynchronous caching infrastructure. 以下示範一個以 Task<TResult> 為基礎建置的小型但功能強大的非同步快取:Here’s an example of a small but powerful asynchronous cache built on top of Task<TResult>:

public class AsyncCache<TKey, TValue>
{
    private readonly Func<TKey, Task<TValue>> _valueFactory;
    private readonly ConcurrentDictionary<TKey, Lazy<Task<TValue>>> _map;

    public AsyncCache(Func<TKey, Task<TValue>> valueFactory)
    {
        if (valueFactory == null) throw new ArgumentNullException("loader");
        _valueFactory = valueFactory;
        _map = new ConcurrentDictionary<TKey, Lazy<Task<TValue>>>();
    }

    public Task<TValue> this[TKey key]
    {
        get
        {
            if (key == null) throw new ArgumentNullException("key");
            return _map.GetOrAdd(key, toAdd =>
                new Lazy<Task<TValue>>(() => _valueFactory(toAdd))).Value;
        }
    }
}

AsyncCache<TKey,TValue> 類別可接受採用 TKey 並傳回 Task<TResult> 的函式作為對其建構函式的委派。The AsyncCache<TKey,TValue> class accepts as a delegate to its constructor a function that takes a TKey and returns a Task<TResult>. 先前從快取中存取的任何值會儲存在內部字典中,AsyncCache 會確保每個索引鍵只產生一個工作,即使並行存取快取也一樣。Any previously accessed values from the cache are stored in the internal dictionary, and the AsyncCache ensures that only one task is generated per key, even if the cache is accessed concurrently.

例如,您可以為下載的網頁建置快取︰For example, you can build a cache for downloaded web pages:

private AsyncCache<string,string> m_webPages =
    new AsyncCache<string,string>(DownloadStringAsync);

然後,每當您需要網頁的內容時,您可以在非同步方法中使用此快取。You can then use this cache in asynchronous methods whenever you need the contents of a web page. AsyncCache 類別可確保儘可能下載較少的頁面並快取結果。The AsyncCache class ensures that you’re downloading as few pages as possible, and caches the results.

private async void btnDownload_Click(object sender, RoutedEventArgs e)
{
    btnDownload.IsEnabled = false;
    try
    {
        txtContents.Text = await m_webPages["https://www.microsoft.com"];
    }
    finally { btnDownload.IsEnabled = true; }
}

AsyncProducerConsumerCollectionAsyncProducerConsumerCollection

您也可以使用工作來建置資料結構,以協調非同步活動。You can also use tasks to build data structures for coordinating asynchronous activities. 請考量其中一種傳統的平行設計模式︰產生者/取用者。Consider one of the classic parallel design patterns: producer/consumer. 在此模式中,產生者產生由取用者取用的資料,產生者和消費者可以平行執行。In this pattern, producers generate data that is consumed by consumers, and the producers and consumers may run in parallel. 比方說,取用者處理先前由產生者產生的項目 1,而產生者現在產生項目 2。For example, the consumer processes item 1, which was previously generated by a producer who is now producing item 2. 在產生者/取用者模式中,您無可避免地一定要使用某種資料結構來儲存產生者建立的工作,才能讓取用者在新資料出現時收到通知並且找到新資料。For the producer/consumer pattern, you invariably need some data structure to store the work created by producers so that the consumers may be notified of new data and find it when available.

以下是根據工作建置的簡單資料結構,可將非同步方法當做產生者和取用者︰Here’s a simple data structure built on top of tasks that enables asynchronous methods to be used as producers and consumers:

public class AsyncProducerConsumerCollection<T>
{
    private readonly Queue<T> m_collection = new Queue<T>();
    private readonly Queue<TaskCompletionSource<T>> m_waiting =
        new Queue<TaskCompletionSource<T>>();

    public void Add(T item)
    {
        TaskCompletionSource<T> tcs = null;
        lock (m_collection)
        {
            if (m_waiting.Count > 0) tcs = m_waiting.Dequeue();
            else m_collection.Enqueue(item);
        }
        if (tcs != null) tcs.TrySetResult(item);
    }

    public Task<T> Take()
    {
        lock (m_collection)
        {
            if (m_collection.Count > 0)
            {
                return Task.FromResult(m_collection.Dequeue());
            }
            else
            {
                var tcs = new TaskCompletionSource<T>();
                m_waiting.Enqueue(tcs);
                return tcs.Task;
            }
        }
    }
}

利用該資料結構,您可以撰寫如下所示的程式碼︰With that data structure in place, you can write code such as the following:

private static AsyncProducerConsumerCollection<int> m_data = …;
…
private static async Task ConsumerAsync()
{
    while(true)
    {
        int nextItem = await m_data.Take();
        ProcessNextItem(nextItem);
    }
}
…
private static void Produce(int data)
{
    m_data.Add(data);
}

System.Threading.Tasks.Dataflow 命名空間包含 BufferBlock<T> 類型,使用方式很類似,但不需要建置自訂集合類型︰The System.Threading.Tasks.Dataflow namespace includes the BufferBlock<T> type, which you can use in a similar manner, but without having to build a custom collection type:

private static BufferBlock<int> m_data = …;
…
private static async Task ConsumerAsync()
{
    while(true)
    {
        int nextItem = await m_data.ReceiveAsync();
        ProcessNextItem(nextItem);
    }
}
…
private static void Produce(int data)
{
    m_data.Post(data);
}

注意

.NET Framework 4.5 有提供 System.Threading.Tasks.Dataflow 命名空間,可透過 NuGet 取得。The System.Threading.Tasks.Dataflow namespace is available in the .NET Framework 4.5 through NuGet. 若要安裝包含 System.Threading.Tasks.Dataflow 命名空間的組件,請在 Visual Studio 中開啟您的專案,從 [專案] 功能表中選擇 [管理 NuGet 套件],然後在線上搜尋 Microsoft.Tpl.Dataflow 套件。To install the assembly that contains the System.Threading.Tasks.Dataflow namespace, open your project in Visual Studio, choose Manage NuGet Packages from the Project menu, and search online for the Microsoft.Tpl.Dataflow package.

另請參閱See also