等待不同结果的多个任务

我有3个任务:

private async Task<Cat> FeedCat() {}
private async Task<House> SellHouse() {}
private async Task<Tesla> BuyCar() {}

它们都需要在我的代码继续之前运行,我也需要每个结果。这些结果之间没有任何共同之处

我如何调用和等待3个任务完成,然后得到结果?

173283 次浏览

只需要分别await三个任务,在全部启动后:

var catTask = FeedCat();
var houseTask = SellHouse();
var carTask = BuyCar();


var cat = await catTask;
var house = await houseTask;
var car = await carTask;

注意:如果任何任务抛出异常,这段代码可能会在后面的任务完成之前返回异常,但它们都将运行。在几乎所有情况下,当你已经知道结果是可取的时,不要等待。在边缘情况下,可能不是这样。

在你使用WhenAll之后,你可以用await单独提取结果:

var catTask = FeedCat();
var houseTask = SellHouse();
var carTask = BuyCar();


await Task.WhenAll(catTask, houseTask, carTask);


var cat = await catTask;
var house = await houseTask;
var car = await carTask;

你也可以使用Task.Result(因为你知道在这一点上它们都成功完成了)。但是,我建议使用await,因为它显然是正确的,而Result在其他情况下可能会导致问题。

你可以把它们存储在任务中,然后等待它们:

var catTask = FeedCat();
var houseTask = SellHouse();
var carTask = BuyCar();


await Task.WhenAll(catTask, houseTask, carTask);


Cat cat = await catTask;
House house = await houseTask;
Car car = await carTask;

你可以使用前面提到的Task.WhenAll,也可以使用Task.WaitAll,这取决于你是否想让线程等待。看看这两个解释的链接。

WaitAll vs WhenAll

使用Task.WhenAll,然后等待结果:

var tCat = FeedCat();
var tHouse = SellHouse();
var tCar = BuyCar();
await Task.WhenAll(tCat, tHouse, tCar);
Cat cat = await tCat;
House house = await tHouse;
Tesla car = await tCar;
//as they have all definitely finished, you could also use Task.Value.

如果你正在使用c# 7,你可以使用一个方便的包装方法,像这样…

public static class TaskEx
{
public static async Task<(T1, T2)> WhenAll<T1, T2>(Task<T1> task1, Task<T2> task2)
{
return (await task1, await task2);
}
}

...当您希望等待多个具有不同返回类型的任务时,可以启用这样的方便语法。当然,您必须对等待的不同数量的任务进行多次重载。

var (someInt, someString) = await TaskEx.WhenAll(GetIntAsync(), GetStringAsync());

但是,如果您打算把这个例子变成现实,请参阅Marc Gravell对ValueTask和已经完成的任务的一些优化的回答。

提出警告

对于那些访问这个线程和其他类似线程寻找使用async+await+task工具集并行化EntityFramework的方法的人来说,这里显示的模式是合理的,然而,当涉及到EF的特殊雪花时,除非并且直到你在每个*Async()调用中使用一个单独的(新的)db-context-instance,否则你将无法实现并行执行。

这类事情是必要的,因为ef-db-context固有的设计限制禁止在同一个ef-db-context实例中并行运行多个查询。


利用已经给出的答案,这是确保你收集所有值的方法,即使在一个或多个任务导致异常的情况下:

  public async Task<string> Foobar() {
async Task<string> Awaited(Task<Cat> a, Task<House> b, Task<Tesla> c) {
return DoSomething(await a, await b, await c);
}


using (var carTask = BuyCarAsync())
using (var catTask = FeedCatAsync())
using (var houseTask = SellHouseAsync())
{
if (carTask.Status == TaskStatus.RanToCompletion //triple
&& catTask.Status == TaskStatus.RanToCompletion //cache
&& houseTask.Status == TaskStatus.RanToCompletion) { //hits
return Task.FromResult(DoSomething(catTask.Result, carTask.Result, houseTask.Result)); //fast-track
}


cat = await catTask;
car = await carTask;
house = await houseTask;
//or Task.AwaitAll(carTask, catTask, houseTask);
//or await Task.WhenAll(carTask, catTask, houseTask);
//it depends on how you like exception handling better


return Awaited(catTask, carTask, houseTask);
}
}

具有或多或少相同性能特征的替代实现可以是:

 public async Task<string> Foobar() {
using (var carTask = BuyCarAsync())
using (var catTask = FeedCatAsync())
using (var houseTask = SellHouseAsync())
{
cat = catTask.Status == TaskStatus.RanToCompletion ? catTask.Result : (await catTask);
car = carTask.Status == TaskStatus.RanToCompletion ? carTask.Result : (await carTask);
house = houseTask.Status == TaskStatus.RanToCompletion ? houseTask.Result : (await houseTask);


return DoSomething(cat, car, house);
}
}

给定三个任务——FeedCat()SellHouse()BuyCar(),有两种有趣的情况:要么它们都同步完成(出于某种原因,可能是缓存或错误),要么它们不同步完成。

假设我们有,从这个问题

Task<string> DoTheThings() {
Task<Cat> x = FeedCat();
Task<House> y = SellHouse();
Task<Tesla> z = BuyCar();
// what here?
}

现在,一个简单的方法是:

Task.WhenAll(x, y, z);

但是…这样不方便处理结果;我们通常希望await:

async Task<string> DoTheThings() {
Task<Cat> x = FeedCat();
Task<House> y = SellHouse();
Task<Tesla> z = BuyCar();


await Task.WhenAll(x, y, z);
// presumably we want to do something with the results...
return DoWhatever(x.Result, y.Result, z.Result);
}

但这会产生大量开销,并分配各种数组(包括params Task[]数组)和列表(在内部)。这是可行的,但在我看来并不是很好。在很多情况下,使用async操作和依次使用await都是更简单的:

async Task<string> DoTheThings() {
Task<Cat> x = FeedCat();
Task<House> y = SellHouse();
Task<Tesla> z = BuyCar();


// do something with the results...
return DoWhatever(await x, await y, await z);
}

与上面的一些注释相反,使用await而不是Task.WhenAll会使没有区别决定任务的运行方式(并发地、顺序地等)。在最高级别上,Task.WhenAll 早于async/await提供了良好的编译器支持,并且是有用的那时这些东西还不存在。当你有一个任意的任务数组,而不是3个离散的任务时,它也很有用。

但是:我们仍然有async/await为继续生成大量编译器噪声的问题。如果可能任务很可能是同步完成的,那么我们可以通过构建带有异步回退的同步路径来优化:

Task<string> DoTheThings() {
Task<Cat> x = FeedCat();
Task<House> y = SellHouse();
Task<Tesla> z = BuyCar();


if(x.Status == TaskStatus.RanToCompletion &&
y.Status == TaskStatus.RanToCompletion &&
z.Status == TaskStatus.RanToCompletion)
return Task.FromResult(
DoWhatever(a.Result, b.Result, c.Result));
// we can safely access .Result, as they are known
// to be ran-to-completion


return Awaited(x, y, z);
}


async Task Awaited(Task<Cat> a, Task<House> b, Task<Tesla> c) {
return DoWhatever(await x, await y, await z);
}

这种“同步路径与异步回退”方法越来越普遍,特别是在同步完成相对频繁的高性能代码中。注意,如果补全总是异步的,这一点帮助都没有。

适用于这里的其他事项:

  1. 在最近的c#中,一个常见的模式是async回退方法通常被实现为一个局部函数:

    Task<string> DoTheThings() {
    async Task<string> Awaited(Task<Cat> a, Task<House> b, Task<Tesla> c) {
    return DoWhatever(await a, await b, await c);
    }
    Task<Cat> x = FeedCat();
    Task<House> y = SellHouse();
    Task<Tesla> z = BuyCar();
    
    
    if(x.Status == TaskStatus.RanToCompletion &&
    y.Status == TaskStatus.RanToCompletion &&
    z.Status == TaskStatus.RanToCompletion)
    return Task.FromResult(
    DoWhatever(a.Result, b.Result, c.Result));
    // we can safely access .Result, as they are known
    // to be ran-to-completion
    
    
    return Awaited(x, y, z);
    }
    
  2. prefer ValueTask<T> to Task<T> if there is a good chance of things ever completely synchronously with many different return values:

    ValueTask<string> DoTheThings() {
    async ValueTask<string> Awaited(ValueTask<Cat> a, Task<House> b, Task<Tesla> c) {
    return DoWhatever(await a, await b, await c);
    }
    ValueTask<Cat> x = FeedCat();
    ValueTask<House> y = SellHouse();
    ValueTask<Tesla> z = BuyCar();
    
    
    if(x.IsCompletedSuccessfully &&
    y.IsCompletedSuccessfully &&
    z.IsCompletedSuccessfully)
    return new ValueTask<string>(
    DoWhatever(a.Result, b.Result, c.Result));
    // we can safely access .Result, as they are known
    // to be ran-to-completion
    
    
    return Awaited(x, y, z);
    }
    
  3. if possible, prefer IsCompletedSuccessfully to Status == TaskStatus.RanToCompletion; this now exists in .NET Core for Task, and everywhere for ValueTask<T>

如果您试图记录所有错误,请确保您保留任务。代码中的WhenAll行,很多注释建议您可以删除它并等待单个任务。的任务。WhenAll对于错误处理非常重要。如果没有这一行,您可能会为未观察到的异常打开代码。

var catTask = FeedCat();
var houseTask = SellHouse();
var carTask = BuyCar();


await Task.WhenAll(catTask, houseTask, carTask);


var cat = await catTask;
var house = await houseTask;
var car = await carTask;

假设FeedCat在以下代码中抛出异常:

var catTask = FeedCat();
var houseTask = SellHouse();
var carTask = BuyCar();


var cat = await catTask;
var house = await houseTask;
var car = await carTask;

在这种情况下,您将永远不会等待houseTask或carTask。这里有3种可能的场景:

    当FeedCat失败时,
  1. SellHouse已经成功完成。在

  2. SellHouse是不完整的和失败的例外在某些时候。未观察到异常,并将在终止器线程上重新抛出。

  3. SellHouse不完整,其中包含等待。以防 你的代码在ASP中运行。NET SellHouse将失败,只要一些 等待将在里面完成。这是因为你基本上 火&当FeedCat失败时,忘记调用和同步上下文丢失。

下面是你将得到的case(3)的错误:

System.AggregateException: A Task's exception(s) were not observed either by Waiting on the Task or accessing its Exception property. As a result, the unobserved exception was rethrown by the finalizer thread. ---> System.NullReferenceException: Object reference not set to an instance of an object.
at System.Web.ThreadContext.AssociateWithCurrentThread(Boolean setImpersonationContext)
at System.Web.HttpApplication.OnThreadEnterPrivate(Boolean setImpersonationContext)
at System.Web.HttpApplication.System.Web.Util.ISyncContext.Enter()
at System.Web.Util.SynchronizationHelper.SafeWrapCallback(Action action)
at System.Threading.Tasks.Task.Execute()
--- End of inner exception stack trace ---
---> (Inner Exception #0) System.NullReferenceException: Object reference not set to an instance of an object.
at System.Web.ThreadContext.AssociateWithCurrentThread(Boolean setImpersonationContext)
at System.Web.HttpApplication.OnThreadEnterPrivate(Boolean setImpersonationContext)
at System.Web.HttpApplication.System.Web.Util.ISyncContext.Enter()
at System.Web.Util.SynchronizationHelper.SafeWrapCallback(Action action)
at System.Threading.Tasks.Task.Execute()<---

对于情况(2),您将得到类似的错误,但原始异常堆栈跟踪。

对于. net 4.0及更高版本,您可以使用TaskScheduler.UnobservedTaskException捕获未观察到的异常。对于。net 4.5及以后的版本,未观察到的异常将被默认接受,而对于。net 4.0,未观察到的异常将使您的进程崩溃。

更多细节:.NET 4.5中的任务异常处理

var dn = await Task.WhenAll<dynamic>(FeedCat(),SellHouse(),BuyCar());

如果你想访问Cat,你可以这样做:

var ct = (Cat)dn[0];

这是非常简单的做法和非常有用的使用,没有必要去追求一个复杂的解决方案。

await语句不是使代码按顺序运行吗?考虑以下代码

class Program
{
static Stopwatch _stopwatch = new();


static async Task Main(string[] args)
{
Console.WriteLine($"fire hot");
_stopwatch.Start();
var carTask = BuyCar();
var catTask = FeedCat();
var houseTask = SellHouse();
await carTask;
await catTask;
await houseTask;
Console.WriteLine($"{_stopwatch.ElapsedMilliseconds} done!");


Console.WriteLine($"using await");
_stopwatch.Restart();
await BuyCar();
await FeedCat();
await SellHouse();


Console.WriteLine($"{_stopwatch.ElapsedMilliseconds} done!");
}


static async Task BuyCar()
{
Console.WriteLine($"{_stopwatch.ElapsedMilliseconds} buy car started");
await Task.Delay(2000);
Console.WriteLine($"{_stopwatch.ElapsedMilliseconds} buy car done");
}


static async Task FeedCat()
{
Console.WriteLine($"{_stopwatch.ElapsedMilliseconds} feed cat started");
await Task.Delay(1000);
Console.WriteLine($"{_stopwatch.ElapsedMilliseconds} feed cat done");
}


static async Task SellHouse()
{
Console.WriteLine($"{_stopwatch.ElapsedMilliseconds} sell house started");
await Task.Delay(10);
Console.WriteLine($"{_stopwatch.ElapsedMilliseconds} sell house done");
}
}


fire hot
0 buy car started
3 feed cat started
4 sell house started
18 sell house done
1004 feed cat done
2013 buy car done
2014 done!
using await
0 buy car started
2012 buy car done
2012 feed cat started
3018 feed cat done
3018 sell house started
3033 sell house done
3034 done!

示例中的三个任务在重要性上差别很大。万一其中一个失败了,你可能想知道其他的发生了什么。例如,如果与自动喂猫器通信失败,你不想错过你的房子是成功还是失败。因此,不仅返回CatHouseTesla,而且返回任务本身是有意义的。调用代码将能够分别查询这三个任务,并对它们成功或失败的完成做出适当的反应:

public async Task<(Task<Cat>, Task<House>, Task<Tesla>)> FeedCatSellHouseBuyCar()
{
Task<Cat> task1 = FeedCat();
Task<House> task2 = SellHouse();
Task<Tesla> task3 = BuyCar();


// All three tasks are launched at this point.


try { await Task.WhenAll(task1, task2, task3).ConfigureAwait(false); } catch { }


// All three tasks are completed at this point.
    

return (task1, task2, task3);
}

使用的例子:

var (catTask, houseTask, teslaTask) = await FeedCatSellHouseBuyCar();


// All three tasks are completed at this point.


if (catTask.IsCompletedSuccessfully)
Console.WriteLine($"{catTask.Result.Name} is eating her healthy meal.");
else
Console.WriteLine("Your cat is starving!");


if (houseTask.IsCompletedSuccessfully)
Console.WriteLine($"Your house at {houseTask.Result.Address} was sold. You are now rich and homeless!");
else
Console.WriteLine("You are still the poor owner of your house.");


if (teslaTask.IsCompletedSuccessfully)
Console.WriteLine($"You are now the owner a battery-powered {teslaTask.Result.Name}.");
else
Console.WriteLine("You are still driving a Hyundai.");

带有空catchtry块是必需的,因为. net 7仍然没有提供正确的方法await一个任务,而不会在取消或失败的情况下抛出。