共用方式為


在非同步工作完成時處理它們 (C#)

透過使用 Task.WhenAny,您可以同時啟動多個任務,並在任務完成時逐一處理它們,而不是按照任務開始的順序進行處理。

下列範例會使用查詢來建立工作集合。 每個任務都會下載指定網站的內容。 在 while 迴圈的每個反覆專案中,等待呼叫會 WhenAny 傳回工作集合中的工作,該工作會先完成其下載。 該工作會從集合中移除並處理。 循環重複,直到集合不再包含任務為止。

先決條件

您可以使用下列其中一個選項來遵循本教學課程:

建立範例應用程式

建立新的 .NET Core 主控台應用程式。 您可以使用 dotnet new 主控台 命令或從 Visual Studio 建立一個。

在程式碼編輯器中開啟 Program.cs 檔案,然後將現有程式碼取代為下列程式碼:

using System.Diagnostics;

namespace ProcessTasksAsTheyFinish;

class Program
{
    static void Main(string[] args)
    {
        Console.WriteLine("Hello World!");
    }
}

新增欄位

在類別定義中 Program ,新增下列兩個欄位:

static readonly HttpClient s_client = new HttpClient
{
    MaxResponseContentBufferSize = 1_000_000
};

static readonly IEnumerable<string> s_urlList = new string[]
{
    "https://learn.microsoft.com",
    "https://learn.microsoft.com/aspnet/core",
    "https://learn.microsoft.com/azure",
    "https://learn.microsoft.com/azure/devops",
    "https://learn.microsoft.com/dotnet",
    "https://learn.microsoft.com/dynamics365",
    "https://learn.microsoft.com/education",
    "https://learn.microsoft.com/enterprise-mobility-security",
    "https://learn.microsoft.com/gaming",
    "https://learn.microsoft.com/graph",
    "https://learn.microsoft.com/microsoft-365",
    "https://learn.microsoft.com/office",
    "https://learn.microsoft.com/powershell",
    "https://learn.microsoft.com/sql",
    "https://learn.microsoft.com/surface",
    "https://learn.microsoft.com/system-center",
    "https://learn.microsoft.com/visualstudio",
    "https://learn.microsoft.com/windows",
    "https://learn.microsoft.com/maui"
};

公開 HttpClient 傳送 HTTP 要求及接收 HTTP 回應的能力。 會 s_urlList 保留應用程式計劃處理的所有 URL。

更新應用程式進入點

主控台應用程式的主要進入點是 Main 方法。 將現有方法取代為下列項目:

static Task Main() => SumPageSizesAsync();

更新的方法 Main 現在被視為 非同步主,它允許執行檔的非同步進入點。 它表達為對 SumPageSizesAsync的呼喚。

建立非同步總和頁面大小方法

在方法下方 Main ,新增方法 SumPageSizesAsync

static async Task SumPageSizesAsync()
{
    var stopwatch = Stopwatch.StartNew();

    IEnumerable<Task<int>> downloadTasksQuery =
        from url in s_urlList
        select ProcessUrlAsync(url, s_client);

    List<Task<int>> downloadTasks = downloadTasksQuery.ToList();

    int total = 0;
    while (downloadTasks.Any())
    {
        Task<int> finishedTask = await Task.WhenAny(downloadTasks);
        downloadTasks.Remove(finishedTask);
        total += await finishedTask;
    }

    stopwatch.Stop();

    Console.WriteLine($"\nTotal bytes returned:  {total:#,#}");
    Console.WriteLine($"Elapsed time:          {stopwatch.Elapsed}\n");
}

迴圈會 while 移除每個疊代中的其中一個工作。 每項任務完成後,循環結束。 該方法首先實例化並啟動一個 Stopwatch. 然後,它包含一個查詢,執行時會建立任務集合。 下列程式碼中的每個呼叫 ProcessUrlAsync 都會傳回 Task<TResult>,其中 TResult 是整數:

IEnumerable<Task<int>> downloadTasksQuery =
    from url in s_urlList
    select ProcessUrlAsync(url, s_client);

由於 LINQ 的 延遲執行 ,您會呼叫 Enumerable.ToList 來啟動每個工作。

List<Task<int>> downloadTasks = downloadTasksQuery.ToList();

while迴圈會針對集合中的每個工作執行下列步驟:

  1. 等待呼叫以 WhenAny 識別集合中已完成下載的第一個工作。

    Task<int> finishedTask = await Task.WhenAny(downloadTasks);
    
  2. 從集合中移除該工作。

    downloadTasks.Remove(finishedTask);
    
  3. Awaits finishedTask,由呼叫 ProcessUrlAsync傳回。 finishedTask變數是 Task<TResult> where TResult 是整數。 工作已經完成,但您等待它擷取下載網站的長度,如下列範例所示。 如果任務出錯, await 將拋出儲存在 中 AggregateException的第一個子異常,這與讀取 Task<TResult>.Result 屬性不同,後者會拋出 AggregateException.

    total += await finishedTask;
    

新增製程方法

在方法下方SumPageSizesAsync新增以下ProcessUrlAsync方法:

static async Task<int> ProcessUrlAsync(string url, HttpClient client)
{
    byte[] content = await client.GetByteArrayAsync(url);
    Console.WriteLine($"{url,-60} {content.Length,10:#,#}");

    return content.Length;
}

對於任何指定的 URL,該方法將使用提供的實例來 client 獲取響應作為 byte[]. 長度會在 URL 和長度寫入主控台之後傳回。

執行程式數次,以驗證下載的長度並不總是以相同的順序顯示。

謹慎

您可以在 WhenAny 迴圈中使用,如範例中所述,以解決涉及少量作業的問題。 但是,如果您有大量任務要處理,其他方法會更有效率。 如需相關資訊和範例,請參閱 在 完成時處理作業

使用 Task.WhenEach

while您可以使用 .NET 9 中引進的新Task.WhenEach方法來簡化方法中SumPageSizesAsync實作的迴圈,方法是在迴圈中await foreach呼叫它。
取代先前實作 while 的迴圈:

    while (downloadTasks.Any())
    {
        Task<int> finishedTask = await Task.WhenAny(downloadTasks);
        downloadTasks.Remove(finishedTask);
        total += await finishedTask;
    }

與簡化 await foreach的:

    await foreach (Task<int> t in Task.WhenEach(downloadTasks))
    {
        total += await t;
    }

這種新方法允許不再重複呼叫 Task.WhenAny 手動呼叫任務並刪除完成任務,因為 Task.WhenEach 任務會 按照任務完成的順序逐一查看任務。

完整範例

以下程式碼是範例 Program.cs 檔案的完整文字。

using System.Diagnostics;

HttpClient s_client = new()
{
    MaxResponseContentBufferSize = 1_000_000
};

IEnumerable<string> s_urlList = new string[]
{
    "https://learn.microsoft.com",
    "https://learn.microsoft.com/aspnet/core",
    "https://learn.microsoft.com/azure",
    "https://learn.microsoft.com/azure/devops",
    "https://learn.microsoft.com/dotnet",
    "https://learn.microsoft.com/dynamics365",
    "https://learn.microsoft.com/education",
    "https://learn.microsoft.com/enterprise-mobility-security",
    "https://learn.microsoft.com/gaming",
    "https://learn.microsoft.com/graph",
    "https://learn.microsoft.com/microsoft-365",
    "https://learn.microsoft.com/office",
    "https://learn.microsoft.com/powershell",
    "https://learn.microsoft.com/sql",
    "https://learn.microsoft.com/surface",
    "https://learn.microsoft.com/system-center",
    "https://learn.microsoft.com/visualstudio",
    "https://learn.microsoft.com/windows",
    "https://learn.microsoft.com/maui"
};

await SumPageSizesAsync();

async Task SumPageSizesAsync()
{
    var stopwatch = Stopwatch.StartNew();

    IEnumerable<Task<int>> downloadTasksQuery =
        from url in s_urlList
        select ProcessUrlAsync(url, s_client);

    List<Task<int>> downloadTasks = downloadTasksQuery.ToList();

    int total = 0;
    while (downloadTasks.Any())
    {
        Task<int> finishedTask = await Task.WhenAny(downloadTasks);
        downloadTasks.Remove(finishedTask);
        total += await finishedTask;
    }

    stopwatch.Stop();

    Console.WriteLine($"\nTotal bytes returned:    {total:#,#}");
    Console.WriteLine($"Elapsed time:              {stopwatch.Elapsed}\n");
}

static async Task<int> ProcessUrlAsync(string url, HttpClient client)
{
    byte[] content = await client.GetByteArrayAsync(url);
    Console.WriteLine($"{url,-60} {content.Length,10:#,#}");

    return content.Length;
}

// Example output:
// https://learn.microsoft.com                                      132,517
// https://learn.microsoft.com/powershell                            57,375
// https://learn.microsoft.com/gaming                                33,549
// https://learn.microsoft.com/aspnet/core                           88,714
// https://learn.microsoft.com/surface                               39,840
// https://learn.microsoft.com/enterprise-mobility-security          30,903
// https://learn.microsoft.com/microsoft-365                         67,867
// https://learn.microsoft.com/windows                               26,816
// https://learn.microsoft.com/maui                               57,958
// https://learn.microsoft.com/dotnet                                78,706
// https://learn.microsoft.com/graph                                 48,277
// https://learn.microsoft.com/dynamics365                           49,042
// https://learn.microsoft.com/office                                67,867
// https://learn.microsoft.com/system-center                         42,887
// https://learn.microsoft.com/education                             38,636
// https://learn.microsoft.com/azure                                421,663
// https://learn.microsoft.com/visualstudio                          30,925
// https://learn.microsoft.com/sql                                   54,608
// https://learn.microsoft.com/azure/devops                          86,034

// Total bytes returned:    1,454,184
// Elapsed time:            00:00:01.1290403

另請參閱