共用方式為


在非同步工作完成時進行處理 (C#)

使用 Task.WhenAny,即可同時啟動多個工作,並在完成時逐一進行處理,而不是依啟動順序進行處理。

下列範例使用查詢來建立工作集合。 每項工作都會下載所指定網站的內容。 在 while 迴圈的的每個反覆運算中,等候的 WhenAny 呼叫會先傳回完成其下載的工作集合中的工作。 該工作會從集合中予以移除,並進行處理。 迴圈會重複進行,直到集合未包含其他工作為止。

必要條件

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

建立範例應用程式

建立新的 .NET Core 主控台應用程式。 您可以使用 dotnet new console 命令或從 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 方法現在會視為 Async 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 呼叫都會傳回 TResult 為整數的 Task<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. 等候 ProcessUrlAsync 呼叫所傳回的 finishedTaskfinishedTask 變數是 Task<TResult>,其中 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,以解決牽涉到少量工作的問題。 不過,如果您有大量的工作要處理,其他方法會更有效率。 如需詳細資訊和範例,請參閱 Processing tasks as they complete (在工作完成時加以處理)。

完整範例

下列程式碼是範例 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

另請參閱