在这篇文章中,我们将看看何时应该使用Task.WhenAny
and Task.WhenAll
。我还将解释 和 之间的Task.WaitAny
区别Task.WaitAll
。然后我继续展示这些结构的好坏用法的一些例子。我们还将看到一些情况,例如当我们需要立即处理和解决它时。
什么时候应该使用 Task.WhenAny
我们Task.WhenAny
在有一组任务时使用,但我们只对第一个完成的任务感兴趣。例如,当我们有几个异步 API,它们都做同样的事情时,就会发生这种情况。但是我们希望从第一个返回结果的人那里接收结果。
private static async Task<string> FirstRespondingUrlAsync(string urlA, string urlB) | |
{ | |
// Start both downloads concurrently. | |
Task<string> downloadTaskA = _httpClient.GetHtmlResponseAsync(urlA); | |
Task<string> downloadTaskB = _httpClient.GetHtmlResponseAsync(urlB); | |
// Wait for either of the tasks to complete. | |
Task<string> completedTask = await Task.WhenAny(downloadTaskA, downloadTaskB); | |
// Return the length of the data retrieved from that URL. | |
string data = await completedTask; | |
return data; | |
} |
另一种情况是当我们需要备份机制时。因此,如果其中一项任务失败,另一项任务完成并给我们结果。出于这个原因,当我们收到一个已完成的任务等待时,如果发生任何异常,它不会传播到返回的任务。请记住,当我们在第 4 行通过调用调用该方法时GetHtmlResponseAsync
,该方法正在获取结果的途中。所以WhenAny
在这种情况下,返回完成的第一个任务。最后通过等待返回的任务,我们得到了实际的结果。
另一个需要注意的重要事情是,当第一个任务完成并返回时,其他任务仍在运行。如果我们不取消它们,这里会发生什么,它们会运行到完成并被放弃。所以在这些情况下,当我们不需要其他任务时,我们可以取消它们。
private async Task<HttpResponseMessage> FirstRespondingUrlWithCancellationAsync(string urlA, string urlB, CancellationToken ct) | |
{ | |
// Start both downloads concurrently. | |
Task<HttpResponseMessage> downloadTaskA = _httpClient.GetAsync(urlA, ct); | |
Task<HttpResponseMessage> downloadTaskB = _httpClient.GetAsync(urlB, ct); | |
// Wait for either of the tasks to complete. | |
Task<HttpResponseMessage> completedTask = await Task.WhenAny(downloadTaskA, downloadTaskB); | |
// Return the length of the data retrieved from that URL. | |
HttpResponseMessage data = await completedTask; | |
return data; | |
} | |
async Task UseFirstRespondingUrlWithCancellationAsync() | |
{ | |
var cts = new CancellationTokenSource(); | |
var result = await FirstRespondingUrlWithCancellationAsync("url1", "url2", cts.Token); | |
//Now we can cancel the rest of the tasks since we already got the result we need | |
cts.Cancel(); | |
} |
我们在这里所做的是将取消标记作为参数并将它们传递给GetAsync
. 所以现在你可以在UseFirstRespondingUrlWithCancellationAsync
方法中看到,当我们得到需要的结果时,我们可以取消剩下的任务。
什么时候应该使用 Task.WhenAll
我们可以使用Task.WhenAll
来等待一组任务完成。我们也可以循环等待每个任务。但这将是低效的,因为我们一次分派任务。
public static async Task DownLoadAsync(params string[] downloads) | |
{ | |
var client = new HttpClient(); | |
foreach (var uri in downloads) | |
{ | |
string content = await client.GetStringAsync(uri); | |
UpdateUI(content); | |
} | |
} | |
private static void UpdateUI(string content) | |
{ | |
throw new NotImplementedException(); | |
} |
正如您在上面的方法中看到的那样,我们GetStringAsync
当时调用方法一。我们想要的是调度同时获取字符串的任务。在这些情况下,我们可以使用Task.WhenAll
. 所以上面的代码可以这样重构。
public static async Task DownLoadAsync2(params string[] downloads) | |
{ | |
var tasks = new List<Task<string>>(); | |
foreach (var uri in downloads) | |
{ | |
var client = new HttpClient(); | |
tasks.Add(client .GetStringAsync(uri)); | |
} | |
await Task.WhenAll(tasks); | |
tasks.ForEach(t => UpdateUI(t.Result)); | |
} |
在上面的代码中,我们将任务聚合到一个列表中并异步等待所有任务。另一个重要的事情是如何Task.WhenAll
处理异常。当抛出一个或多个异常时,异常将作为AggregateException
. 如果我们在这种情况下使用 try catch,我们只会从异常集合中获取第一个异常。如果我们需要所有这些,我们不能等待任务并在同一行获得结果。
private static Task ThrowInvalidOperationExceptionAsync() => throw new NotImplementedException(); | |
private static Task ThrowNotImplementedExceptionAsync() => throw new InvalidOperationException(); | |
static async Task AllExceptionsAsync() | |
{ | |
var task1 = ThrowNotImplementedExceptionAsync(); | |
var task2 = ThrowInvalidOperationExceptionAsync(); | |
Task allTasks = Task.WhenAll(task1, task2); | |
try | |
{ | |
await allTasks; | |
} | |
catch | |
{ | |
AggregateException allExceptions = allTasks.Exception; | |
} | |
} |
在上面的代码摘录中,我们将任务分配给一个变量。然后我们等待它,然后使用之前的任务变量来获取所有异常。
处理完成的任务
另一种可能偶尔出现的情况是,我们需要在任务完成后立即对其进行一些处理。因为现在我们使用的方式Task.WhenAll
,我们一次得到所有任务的结果。在启动下一个之前,我们没有机会对结果进行任何更改。最好的解决方案是创建一个中间方法,为我们执行等待和处理,并改为等待该方法。
static async Task<int> ExampleTaskAsync(int val) | |
{ | |
await Task.Delay(TimeSpan.FromSeconds(val)); | |
return val; | |
} | |
static async Task AwaitAndProcessAsync(Task<int> task) | |
{ | |
var result = await task; | |
Trace.WriteLine(result); | |
} | |
static async Task ProcessTasksAsync() | |
{ | |
Task<int> task1 = ExampleTaskAsync(2); | |
Task<int> task2 = ExampleTaskAsync(3); | |
Task<int> task3 = ExampleTaskAsync(1); | |
var tasks = new[] { task1, task2, task3 }; | |
var processingTasks = tasks.Select(AwaitAndProcessAsync).ToList(); | |
await Task.WhenAll(processingTasks); | |
} |
正如您在上面的代码中看到的,AwaitAndProcessAsync
方法是为我们执行等待和额外处理的方法。我们首先创建任务,然后通过AwaitAndProcessAsync
方法对它们进行管道传输并获取任务列表。然后,我们使用Task.WhenAll
等待所有这些任务,并在它们返回值后立即处理它们。Stephen Toub 的一篇文章进行了更详细的介绍。
Task.WhenAny 与 Task.WaitAny
这两者之间的主要区别在于Task.WaitAny
阻塞操作。它与使用task.Wait()
or相同task.Result
。不同之处在于它需要多个任务并从第一个完成的任务中返回结果。Task.WaitAny
在某些情况下可以使用,但很少见。也许当我们想要阻止操作时,例如在控制台应用程序中。但即使这样也是不能接受的。因为从 C# 7.1 开始,我们可以使用async Task Main(string[] args)
.
Task.WhenAll 与 Task.WaitAll
Like Task.WaitAny
,Task.WaitAll
也是阻塞操作。它的行为与 相同task.Wait()
,只是它需要一组任务并等待所有任务完成。就像它的对应部分一样,Task.WaitAll
如果有的话,也很少使用。Task.WhenAll
在大多数情况下,我们应该使用非阻塞。
概括
在这篇文章中,我解释了何时以及如何使用Task.WhenAny
and Task.WhenAll
。我们还看到了它们与方法的差异Task.WaitAny
以及Task.WaitAll
有关使用它们的一些最佳实践。我还简要介绍了使用 Task.WhenAll 时的情况和解决方案,我们需要在任务返回值后立即进行一些处理。