透過使用 Task.WhenAny,您可以同時啟動多個任務,並在任務完成時逐一處理它們,而不是按照任務開始的順序進行處理。
下列範例會使用查詢來建立工作集合。 每個任務都會下載指定網站的內容。 在 while 迴圈的每個反覆專案中,等待呼叫會 WhenAny 傳回工作集合中的工作,該工作會先完成其下載。 該工作會從集合中移除並處理。 循環重複,直到集合不再包含任務為止。
先決條件
您可以使用下列其中一個選項來遵循本教學課程:
- Visual Studio 2022 已安裝 .NET 桌面開發工作負載。 當您選取此工作負載時,會自動安裝 .NET SDK。
- .NET SDK 具有您選擇的程式碼編輯器,例如 Visual Studio Code。
建立範例應用程式
建立新的 .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迴圈會針對集合中的每個工作執行下列步驟:
等待呼叫以
WhenAny識別集合中已完成下載的第一個工作。Task<int> finishedTask = await Task.WhenAny(downloadTasks);從集合中移除該工作。
downloadTasks.Remove(finishedTask);Awaits
finishedTask,由呼叫ProcessUrlAsync傳回。finishedTask變數是 Task<TResult> whereTResult是整數。 工作已經完成,但您等待它擷取下載網站的長度,如下列範例所示。 如果任務出錯,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