共用方式為


使用以任務為基礎的非同步模式

當您使用以工作為基礎的非同步模式(任務非同步模式,TAP)來處理非同步操作時,您可以使用回呼來實現無需封鎖的等待。 對於任務,這是透過 Task.ContinueWith 等方法來達成的。 基於語言的非同步支援允許在正常控制流程中等待非同步操作,從而隱藏回呼,而編譯器生成的程式碼提供了相同的 API 層級支援。

使用 Await 暫停執行

您可以使用 C# 中的 await 關鍵詞和 Visual Basic 中的 Await 運算符 ,以異步方式等候 TaskTask<TResult> 物件。 當您等候 Task 時,await 表示式的類型為 void。 當您等候 Task<TResult> 時,await 表示式的類型為 TResultawait表達式必須發生在異步方法的主體內。 (這些語言功能是在 .NET Framework 4.5 中引進的。

在幕後,await 功能透過建立連續性在工作上安裝回呼函式。 這個回呼會在暫停時繼續異步方法。 當異步方法繼續時,如果等候的作業順利完成且 為 Task<TResult>,則會傳回其 TResult 。 如果等候的TaskTask<TResult>Canceled狀態結束,則會拋出OperationCanceledException例外狀況。 如果所等候的 TaskTask<TResult> 最終處於 Faulted 狀態,便會拋出導致錯誤的例外狀況。 Task可能會因為多個例外狀況而發生錯誤,但只會傳播其中一個例外狀況。 不過, Task.Exception 屬性會 AggregateException 傳回包含所有錯誤的例外狀況。

如果同步處理內容(SynchronizationContext 物件)與在暫停時執行異步方法的線程相關聯(例如,如果 SynchronizationContext.Current 屬性不是 null),異步方法會使用內容 Post 的方法繼續處理該相同的同步處理內容。 否則,它會依賴暫停時目前的工作排程器(TaskScheduler 物件)。 一般而言,這是以線程集區為目標的預設工作排程器 (TaskScheduler.Default)。 此任務排程器會決定非同步操作是否應在完成的位置繼續,或是否應排程以繼續執行。 預設排程器通常允許接續在已完成的等候作業的線程上執行。

呼叫異步方法時,它會同步執行函式主體,直到尚未完成的可等候實例上的第一個 await 表達式為止,此時調用會傳回給呼叫端。 如果異步方法未傳回void,則會傳回TaskTask<TResult>物件來表示進行中的計算。 在非 void 異步方法中,如果遇到 return 語句或到達方法主體的結尾,工作就會以 RanToCompletion 最終狀態完成。 如果未處理的例外狀況導致程式控制離開異步方法的主體,任務會以Faulted狀態結束。 如果該例外狀況是 OperationCanceledException,工作會改為以 Canceled 狀態結束。 如此一來,最終會發佈結果或例外狀況。

此行為有數個重要變化。 基於效能考量,如果在等待任務時任務已完成,則不會釋放控制權,而且函式會繼續執行。 此外,回到原始內容不一定是所需的行為,而且可以變更;下一節會更詳細地說明這一點。

使用 Yield 和 ConfigureAwait 配置中止和恢復

數種方法可提供對異步方法執行的更多控制權。 例如,您可以使用 Task.Yield 方法,將產生點引入異步方法:

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

這相當於以異步方式回傳或排程回到目前的上下文。

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

您也可以使用 Task.ConfigureAwait 方法,在異步方法中更好地控制暫停和繼續。 如先前所述,根據預設,目前的內容會在異步方法暫停時擷取,而擷取的內容則用來在繼續時叫用異步方法的接續。 在許多情況下,這是您想要的確切行為。 在其他情況下,您可能不在意接續內容,而且您可以避免這類貼文回到原始內容,以達到更好的效能。 若要啟用此功能,請使用 Task.ConfigureAwait 方法來通知 await 作業,不要擷取或恢復到同步內容上,而是在哪裡完成等候的異步操作後繼續執行。

await someTask.ConfigureAwait(continueOnCapturedContext:false);

取消異步操作

從 .NET Framework 4 開始,支援取消的 TAP 方法至少提供一個可接受取消令牌 (CancellationToken object) 的多載。

取消令牌是透過取消令牌來源 (CancellationTokenSource 物件) 建立的。 來源的 Token 屬性會傳回 Cancel 取消標記,當呼叫來源的 Cancel 方法時就會發出訊號。 例如,如果您想要下載單一網頁並想要取消作業,您可以建立 CancellationTokenSource 物件、將其令牌傳遞至 TAP 方法,然後在準備好取消作業時呼叫來源 Cancel 的 方法:

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

若要取消多個非同步呼叫,您可以將相同的 token 傳遞至所有呼叫:

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

或者,您可以將相同的令牌傳遞至選定的作業子集。

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();

這很重要

取消要求可以從任何線程起始。

您可以將值傳遞 CancellationToken.None 至任何接受取消令牌的方法,以指出永遠不會要求取消。 這會導致 CancellationToken.CanBeCanceled 屬性傳回 false,而呼叫的方法可以據以優化。 基於測試目的,您也可以使用接受布爾值的建構函式來傳入預先取消的取消令牌,以指出令牌應該以已取消或無法取消的狀態啟動。

這個取消方法有數個優點:

  • 您可以將相同的取消令牌傳遞至任意數目的異步和同步作業。

  • 相同的取消要求可能會傳播到任意數目的監聽者。

  • 異步 API 的開發人員完全控制是否可能要求取消,以及何時可能生效。

  • 使用 API 的程式碼可能會選擇性地決定取消請求將傳遞到哪些異步調用。

監控進度

某些異步方法會透過傳遞至異步方法的進度介面公開進度。 例如,請考慮一個異步函式,用於下載一段文字,並在過程中更新進度,顯示目前已完成的下載百分比。 這類方法可以在 Windows Presentation Foundation (WPF) 應用程式中取用,如下所示:

private async void btnDownload_Click(object sender, RoutedEventArgs e)
{
    btnDownload.IsEnabled = false;
    try
    {
        txtResult.Text = await DownloadStringTaskAsync(txtUrl.Text,
            new Progress<int>(p => pbDownloadProgress.Value = p));
    }
    finally { btnDownload.IsEnabled = true; }
}

使用內建任務型組合子

命名空間 System.Threading.Tasks 包含數種方法,可用來組裝和使用任務。

Task.Run

類別 Task 包含數 Run 種方法,可讓您輕鬆地將工作分配為 TaskTask<TResult> 到執行緒池,例如:

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 方法的簡化存在。 此重載可讓您在移交的工作中使用 await,例如:

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方法。

Task.FromResult

在數據可能已經可用且只需要從工作提升至FromResult的方法傳回時使用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.WhenAll

使用 WhenAll 方法,以異步方式等候多個表示為任務的異步操作。 方法有多個多載,可支援一組非泛型工作,或一組不統一的泛型工作。例如,它可用於異步等候多個 void 傳回的操作,或異步等候多個值傳回的方法,其中每個值可能具有不同的類型。此外,它亦支援一組統一的泛型工作,例如異步等候多個 TResult 傳回的方法。

假設您想要傳送電子郵件訊息給數個客戶。 您可以重迭傳送訊息,以便在傳送下一個訊息之前,不要等待一個訊息完成。 您也可以瞭解傳送作業何時完成,以及是否發生任何錯誤:

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

此程式代碼不會明確處理可能發生的例外狀況,但會讓例外狀況從await中傳播到WhenAll所產生的任務上。 若要處理例外狀況,您可以使用下列程序代碼:

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

在此情況下,如果有任何異步操作失敗,所有例外狀況都會合併在 AggregateException 例外中,該例外狀況會儲存在從 Task 方法傳回的 WhenAll 中。 不過,只有其中一個例外狀況會由關鍵詞 await 傳播。 如果您想要檢查所有例外狀況,您可以重寫先前的程式代碼,如下所示:

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 下載多個檔案的範例。 在此情況下,所有異步作都有同質的結果類型,而且很容易存取結果:

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

您可以使用我們在先前的 void-returning 案例中討論的相同例外狀況處理技術:

Task<string> [] asyncOps =
    (from url in urls select DownloadStringTaskAsync(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.WhenAny

您可以使用 WhenAny 方法,以非同步方式等候多個以任務表示的非同步操作中的其中一個完成。 此方法提供四個主要使用案例:

  • 冗餘:多次執行操作,並選取第一個完成的操作(例如,使用多個股票報價網絡服務以產生單一結果,並選取最先完成的那個)。

  • 交錯:啟動多個作業,並等候所有作業完成,但在各作業完成時立即處理它們。

  • 節流:允許附加操作在其他操作完成時開始。 這是交錯情境的延伸。

  • 早期救助:例如,任務 t1 代表的操作可以和另一個任務 t2 分組成一個 WhenAny 任務,然後您可以在 WhenAny 任務上等待。 工作 t2 可能代表逾時、取消或一些其他信號,這些信號會導致 WhenAny 工作在 t1 完成之前完成。

冗餘性

請考慮您想要決定是否購買股票的情況。 您信任的股票建議 Web 服務有幾個,但視每日負載而定,每個服務最終在不同的時間可能會變慢。 當任何作業完成時,您可以使用 WhenAny 方法來接收通知:

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 則傳回其中已完成的具體任務。 如果任務失敗,務必要知道何時失敗;如果任務成功,務必要了解傳回值與哪個任務相關聯。 因此,您必須存取傳回工作的結果,或進一步等候它,如本範例所示。

如同 WhenAll,您必須能夠容納例外狀況。 因為您收到已完成的工作後,您可以等候傳回的工作以查看是否有錯誤傳播,然後適當地處理這些錯誤;例如:

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

此外,即使第一個工作順利完成,後續工作也可能失敗。 此時,您有數個處理例外狀況的選項:您可以等到所有啟動的工作都完成,在此情況下您可以使用 WhenAll 方法,或者您可以決定所有例外狀況都很重要,而且必須記錄。 因此,當工作以異步方式完成時,您可以使用接續來接收通知:

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

或:

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

或甚至:

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

最後,您可能想要取消所有剩餘的作業:

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

交錯

假設您正在從 Web 下載影像並處理每個影像(例如,將影像新增至 UI 控制件)。 您可以在UI線程上循序處理映像,但想要盡可能同時下載映像。 此外,在所有圖片下載完成之前,您應避免延遲將它們新增至UI。 相反地,您需要在它們逐一完成時新增。

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

您也可以將交錯應用於涉及對所下載影像進行計算量大的處理的情境,例如:

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

限速

請考慮交錯執行的範例,不同之處在於使用者正在下載大量圖片,因此必須控制下載速度;例如,您希望同時進行特定數目的下載。 若要達成此目的,您可以啟動異步操作的子集。 作業完成時,您可以啟動其他作業來取代其位置:

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

早期紓困

假設您正在以異步方式等待作業完成,同時回應使用者的取消要求(例如,使用者已按下取消按鈕)。 下列程式代碼說明此案例:

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

此實作會在您決定退出時立即重新啟用使用者介面,但不會取消基礎的異步操作。 另一個替代方式是,當您決定放棄時取消未完成的作業,但直到作業完成才重新建立使用者介面,這可能是因為取消請求導致提前結束。

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 方法。

Task.Delay(任務延遲)

您可以使用 Task.Delay 方法將暫停功能引入到異步方法的執行過程中。 這適用於許多種類的功能,包括建置輪詢迴圈,以及延遲使用者輸入的預先決定期間處理。 方法 Task.Delay 也可以與 Task.WhenAny 在 await 上實作逾時搭配使用。

如果屬於較大異步作的工作(例如,ASP.NET Web 服務)需要太長的時間才能完成,則整體作業可能會受到影響,特別是如果無法完成。 因此,設置異步操作的時間限制是很重要的。 同步的Task.WaitTask.WaitAllTask.WaitAny方法會接受逾時值,但對應的TaskFactory.ContinueWhenAll/TaskFactory.ContinueWhenAny和先前提及的Task.WhenAll/Task.WhenAny方法則不會。 相反地,您可以使用 Task.DelayTask.WhenAny 組合來實作逾時。

例如,在您的UI應用程式中,您可能想要下載圖片,並在圖片下載時停用UI。 不過,如果下載花費太長的時間,您想要重新啟用UI並捨棄下載:

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 會傳回一個任務:

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.Result) panel.AddImage(bmp);
            status.Text = "Downloaded";
        }
        else
        {
            status.Text = "Timed out";
            downloads.ContinueWith(t => Log(t));
        }
    }
    finally { btnDownload.Enabled = true; }
}

創建以任務為基礎的組合器

因為任務能夠完全代表異步操作,並提供同步和異步能力來與操作聯結、擷取其結果等等,所以您可以構建實用的組合器函式庫,以創建更大的模式。 如上一節所述,.NET 包含數個內建組合器,但您也可以自行建置。 下列各節提供數個可能的結合子方法和類型範例。

RetryOnFault

在許多情況下,如果先前的嘗試失敗,您可能會想要重試作業。 針對同步程式代碼,您可以建置協助程式方法,例如 RetryOnFault 在下列範例中完成這項作業:

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 實作的異步操作建置一個幾乎完全相同的輔助方法,並因此傳回工作任務:

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

然後,您可以使用這個結合器,將重試編碼成應用程式的邏輯;例如:

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

您可以進一步擴充函式 RetryOnFault 。 例如,函式可以接受將在重試之間叫用的另一個 Func<Task> 函式,以判斷何時再次嘗試作業,例如:

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

然後,您可以使用 函式,如下所示,在重試作業之前等候一秒:

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

NeedOnlyOne

有時候,您可以利用備援來改善作業的延遲和成功機會。 請考慮提供股票報價的多個 Web 服務,但在一天中的各種時間,每個服務可能會提供不同層級的質量和響應時間。 若要處理這些波動,您可能會向所有 Web 服務發出要求,一旦收到來自其中一項的回應,請取消其餘的要求。 您可以實作協助程式函式,讓您更輕鬆地實作啟動多個作業、等候任何作業,然後取消其餘作業的常見模式。 下列範例中的函式 NeedOnlyOne 說明此情境:

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

然後,您可以使用此函式,如下所示:

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

交替運作

當您處理大量工作時,若使用 WhenAny 方法來支援交錯狀況,可能會發生效能問題。 每次呼叫 WhenAny 都會產生一個續處理程序被註冊到每個任務中。 對於 N 個任務,這會導致在交錯操作存續期間建立 O(N²) 的延續項。 如果您正在處理一組大型工作,您可以使用組合子(Interleaved 在下列範例中)來解決效能問題:

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

然後,您可以使用結合器在工作完成時處理結果;例如:

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

WhenAllOrFirstException

在某些分散/聚集情境中,您可能想要等候集合中的所有工作。除非其中一個工作發生故障,此時您會希望在異常發生時立即停止等候。 您可以使用組合器方法來完成此作業,例如 WhenAllOrFirstException 在下列範例中:

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

建置以工作為基礎的數據結構

除了能夠建置自定義基於任務的結合子之外,TaskTask<TResult> 中具有的數據結構不僅代表異步操作的結果,還包括與其聯合所需的同步處理,使其成為在異步情境中建構自定義數據結構的強大類型。

AsyncCache

工作的一個重要方面是,它可能會交給多個取用者,所有取用者都可以等待它,向它註冊續增動作,取得其結果或例外狀況(在Task<TResult>的情況下),依此而定。 這使 TaskTask<TResult> 完美適合用於異步快取基礎設施。 以下是一個建置在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("valueFactory");
        _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> 的函式。 任何先前從快取存取的值都會儲存在內部字典中,AsyncCache 確保即使同時存取快取,每個鍵值也只會產生一個工作。

例如,您可以建置已下載網頁的快取:

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

然後,每當您需要網頁的內容時,就可以在異步方法中使用這個快取。 類別 AsyncCache 可確保您盡可能少地下載頁面,並快取結果。

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

AsyncProducerConsumerCollection

您也可以使用任務來建構資料結構,以協調非同步活動。 請考慮其中一種傳統平行設計模式:生產者/取用者。 在此模式中,生產者生成消費者取用的數據,並且生產者和消費者可以並行執行。 例如,消費者處理先前由生產者所產生的項目 1,而該生產者現在正在生產項目 2。 針對生產者/取用者模式,您總是需要一些數據結構來儲存生產者所建立的工作,讓取用者可以收到新數據的通知,並在可用時找到它。

以下是以工作為基礎建置的簡單數據結構,可讓異步方法作為產生者和取用者使用:

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

在該數據結構就緒后,您可以撰寫如下的程式代碼:

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>類型,您可以以類似的方式使用,但不需要建置自定義集合類型:

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

備註

命名空間 System.Threading.Tasks.Dataflow 以 NuGet 套件的形式提供。 若要安裝包含命名空間的 System.Threading.Tasks.Dataflow 元件,請在 Visual Studio 中開啟您的專案,從 [專案] 選單選擇 [ 管理 NuGet 套件 ],然後在線搜尋 System.Threading.Tasks.Dataflow 套件。

另請參閱