我有3个任务:

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

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

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


只需要分别等待这三个任务,在启动它们之后:

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。结果(因为此时您知道它们都已成功完成)。但是,我推荐使用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


使用任务。然后等待结果:

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);

但是…这样不方便处理结果;我们通常会等待:

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 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对任务的运行方式(并发、顺序等)没有影响。在最高级别,任务。WhenAll在编译器支持async/await之前就已经存在了,在那些东西不存在的时候非常有用。当你有一个任意的任务数组,而不是3个离散的任务时,它也很有用。

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

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);
}

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

适用于这里的其他事项:

with recent C#, a common pattern is for the async fallback method is commonly implemented as a local function: 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); } 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); } 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种可能的场景:

SellHouse is already completed successfully when FeedCat failed. In this case you are fine. SellHouse is not complete and fails with exception at some point. Exception is not observed and will be rethrown on finalizer thread. SellHouse is not complete and contains awaits inside it. In case your code runs in ASP.NET SellHouse will fail as soon as some of the awaits will completed inside it. This happens because you basically made fire & forget call and synchronization context was lost as soon as FeedCat failed.

下面是你将得到的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!

The three tasks in your example differ greatly in importance. In case one of them fails, you probably want to know what happened with the others. For example in case the communication with the automatic cat feeder failed, you don't want to miss whether selling your house succeeded or failed. So it makes sense to return back not just a Cat, a House and a Tesla, but the tasks themselves. The calling code will then be able to query separately each of the three tasks, and react appropriately to their successful or failed completions:

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.");

带有空catch的try块是必需的,因为. net 7仍然没有提供适当的方法来等待任务,而不会在取消或失败时抛出。