325

我有 3 个任务:

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

在我的代码可以继续之前,它们都需要运行,我也需要每个结果。所有的结果都没有任何共同点

如何调用并等待 3 个任务完成然后获得结果?

4

11 回答 11

571

使用后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在其他情况下可能会导致问题。

于 2013-06-19T17:42:32.577 回答
133

只是await三个任务分开,在启动它们之后。

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

var cat = await catTask;
var house = await houseTask;
var car = await carTask;
于 2013-06-19T17:40:30.437 回答
47

如果您使用的是 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 和已完成任务的一些优化。

于 2016-12-02T18:19:47.377 回答
26

给定三个任务 -和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[]数组)和列表(内部)。它有效,但它不是很好的IMO。在许多方面,使用操作更简单,而且每个操作都更简单:asyncawait

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的运行方式(并发、顺序等)没有影响。在最高级别上,早于对/的良好编译器支持,并且在这些东西不存在时很有用。当您有任意任务数组而不是 3 个离散任务时,它也很有用。Task.WhenAll asyncawait

但是:我们仍然有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#,一个常见的模式是asyncfallback 方法通常被实现为一个本地函数:

    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. 如果有很大的机会与许多不同的返回值完全同步,ValueTask<T>则更喜欢:Task<T>

    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. 如果可能,更IsCompletedSuccessfully喜欢Status == TaskStatus.RanToCompletion; 这现在存在于 .NET Core 中Task,并且无处不在ValueTask<T>

于 2018-02-06T16:11:56.753 回答
13

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

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;
于 2013-06-19T17:43:06.593 回答
9

如果您尝试记录所有错误,请确保在代码中保留 Task.WhenAll 行,许多评论建议您可以将其删除并等待单个任务。Task.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种可能的情况:

  1. 当 FeedCat 失败时,SellHouse 已经成功完成。在这种情况下,你很好。

  2. SellHouse 不完整,并且在某些时候出现异常而失败。未观察到异常,并将在终结器线程上重新抛出。

  3. SellHouse 不完整,其中包含等待。如果您的代码在 ASP.NET 中运行,SellHouse 将在其中完成一些等待时立即失败。发生这种情况是因为您基本上发出了 fire & forget 调用,并且一旦 FeedCat 失败,同步上下文就丢失了。

这是您将在案例 (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 中的任务异常处理

于 2018-04-21T05:55:07.763 回答
4

您可以使用Task.WhenAll前面提到的 或Task.WaitAll,具体取决于您是否希望线程等待。查看链接以了解两者的解释。

WaitAll 与 WhenAll

于 2013-06-19T17:44:03.393 回答
1

前向警告

只是对那些访问这个和其他类似线程的人的快速提醒,他们正在寻找一种使用 async+await+task 工具集并行化 EntityFramework的方法:这里显示的模式是合理的,但是,当涉及到 EF 的特殊雪花时,你不会实现并行执行,除非您在所涉及的每个 *Async() 调用中使用单独的(新)db-context-instance。

由于 ef-db-contexts 的固有设计限制,禁止在同一个 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);
     }
 }
于 2017-11-20T17:30:07.867 回答
0

使用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.
于 2013-06-19T17:44:49.377 回答
-1
var dn = await Task.WhenAll<dynamic>(FeedCat(),SellHouse(),BuyCar());

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

var ct = (Cat)dn[0];

这做起来非常简单,使用起来也非常有用,无需追求复杂的解决方案。

于 2019-02-25T03:19:47.870 回答
-1

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

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!
于 2021-05-17T12:02:38.933 回答