等待具有不同结果的多个任务

Posted

技术标签:

【中文标题】等待具有不同结果的多个任务【英文标题】:Awaiting multiple Tasks with different results 【发布时间】:2013-06-16 08:51:13 【问题描述】:

我有 3 个任务:

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

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

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

【问题讨论】:

您有订购要求吗?也就是喂完猫之后才卖房子吗? 【参考方案1】:

只需await这三个任务,都启动后。

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

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

【讨论】:

@Bargitta 不,那是错误的。他们将并行开展工作。随意运行它并亲自查看。 People 多年来一直在问同样的问题...我觉得再次强调@正文中的任务“starts on create”很重要987654322@: 也许他们懒得看cmets @StephenYork 添加Task.WhenAll 实际上不会以任何可观察到的方式改变程序的行为。这是一个纯粹的冗余方法调用。如果您愿意,欢迎您添加它作为审美选择,但它不会改变代码的作用。无论是否调用该方法,代码的执行时间都是相同的(好吧,从技术上讲,调用WhenAll 会有非常小的开销,但这应该可以忽略不计),只会使版本比此版本运行时间略长 @StephenYork 您的示例按顺序运行操作有两个原因。您的异步方法实际上不是异步的,它们是同步的。您拥有始终返回已完成任务的同步方法这一事实阻止了它们同时运行。接下来,您实际上并没有执行启动所有三个异步方法的答案中显示的操作,然后 然后 依次等待三个任务。与此代码不同,您的示例在前一个完成之前不会调用每个方法,因此显式阻止一个在前一个完成之前启动。 @MarcvanNieuwenhuijzen 这显然不是真的,正如这里的 cmets 和其他答案中所讨论的那样。添加WhenAll 是纯粹的审美变化。唯一可观察到的行为差异是,如果较早的任务出错,您是否等待后面的任务完成,这通常不需要这样做。如果您不相信关于为什么您的陈述不正确的众多解释,您可以简单地自己运行代码并查看它是否不正确。【参考方案2】:

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

【讨论】:

你可以完全删除WhenAll; awaits 将确保您在完成任务之前不会超过后面的 3 个任务。 Task.WhenAll() 允许以并行模式运行任务。我不明白为什么@Servy 建议删除它。没有WhenAll,它们将被一一运行 @Sergey:任务立即开始执行。例如,catTaskFeedCat 返回时已经在运行。因此,任何一种方法都行得通——唯一的问题是你是想一次await他们还是一起。错误处理略有不同 - 如果您使用 Task.WhenAll,那么它将全部使用 await,即使其中一个提前失败。 @Sergey 调用WhenAll 对操作执行的时间或执行方式没有影响。它只有有任何可能性影响结果的观察方式。在这种特殊情况下,唯一的区别是前两种方法之一中的错误会导致在我的方法中比斯蒂芬更早地在此调用堆栈中抛出异常(尽管总是会抛出相同的错误,如果有的话)。 @Sergey:关键是异步方法总是返回“热”(已经开始的)任务。【参考方案3】:

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

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;

【讨论】:

var catTask = FeedCat() 执行函数 FeedCat() 并将结果存储到 catTask 使得 await Task.WhenAll() 部分无用,因为该方法已经执行?? @sanuel 如果他们返回任务 ,那么不......他们开始异步打开,但不要等待它 我不认为这是准确的,请参阅@StephenCleary 的回答下的讨论...另请参阅Servy 的回答。 如果我需要添加 .ConfigrtueAwait(false)。我会将它添加到 Task.WhenAll 还是随后的每个等待者中? @AstroSharp 一般来说,将它添加到所有这些是一个好主意(如果第一个完成,它会被有效地忽略),但在这种情况下,可能只是做第一个 - 除非以后有更多的异步事情发生。【参考方案4】:

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

WaitAll vs WhenAll

【讨论】:

【参考方案5】:

使用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.

【讨论】:

mm...不是Task.Value(可能2013年就已经存在了?),而是tCat.Result、tHouse.Result或tCar.Result【参考方案6】:

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

【讨论】:

元组是这里涉及的唯一 C# 7 功能。这些肯定在最终版本中。 我知道元组和 c# 7。我的意思是我找不到返回元组的方法 WhenAll。什么命名空间/包? @YuryShcherbakov Task.WhenAll() 没有返回元组。一个是在Task.WhenAll() 返回的任务完成后,从提供的任务的Result 属性构建的。 我建议按照斯蒂芬的推理替换 .Result 调用,以避免其他人通过复制您的示例来延续这种不良做法。 @nrofis 那是错误的。两个任务都被创建,因此在等待之前启动。相当于Servy's answer。【参考方案7】:

前向警告

对于那些访问此线程和其他类似线程并寻找使用 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);
     
 

【讨论】:

【参考方案8】:

给定三个任务 - 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。在许多方面,使用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);

与上面的某些 cmets 不同,使用 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);

这种“带有异步回退的同步路径”方​​法越来越普遍,尤其是在同步完成相对频繁的高性能代码中。请注意,如果完成始终是真正异步的,这将毫无帮助。

此处适用的其他内容:

    在最近的 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);
    
    

    如果有很多不同的返回值完全同步的事情很有可能发生,则更喜欢ValueTask&lt;T&gt; 而不是Task&lt;T&gt;

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

    如果可能,首选IsCompletedSuccessfully 而不是Status == TaskStatus.RanToCompletion;这现在存在于 .NET Core 中的 Task,以及无处不在的 ValueTask&lt;T&gt;

【讨论】:

“与这里的各种答案相反,使用 await 而不是 Task.WhenAll 对任务的运行方式没有影响(并发、顺序等)”我没有看到任何这样说的答案。如果他们这样做,我已经对他们发表了评论。很多答案都有很多 cmets 这么说,但没有答案。你指的是哪个?另请注意,您的答案不处理任务的结果(或处理结果都是不同类型的事实)。您已经将它们组合在一个方法中,当它们全部完成而不使用结果时,它只返回一个 Task @Servy 你是对的,那是 cmets;我将添加一个调整以显示使用结果 @Servy 调整添加 另外,如果您打算尽早处理同步任务,您不妨同时处理任何同步取消或出错的任务,而不仅仅是那些成功完成的任务。如果您已决定这是您的程序需要的优化(这种情况很少见,但会发生),那么您不妨一路走下去。 @MarcGravell 我已经更仔细地阅读了您的答案,并意识到我误解了您关于同步完成任务的陈述。我不明白的是你说Task.WhenAll 没有区别。但我确实看到Task.WhenAll 和每次迭代中的等待之间有明显的区别。如果我创建 10 个等待时间为 500 毫秒的等待并将它们与Task.WhenAll 一起启动,它们会在不到一秒的时间内完成。而如果我等待每 10 个等待 - 它们是按顺序执行的(正如我所期望的那样)并在大约 5 秒内完成。【参考方案9】:

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

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

    SellHouse 不完整,有时会出现异常失败。未观察到异常,将在终结器线程上重新抛出。

    SellHouse 不完整,其中包含等待。如果 您在 ASP.NET SellHouse 中运行的代码将在某些 awaits 将在其中完成。发生这种情况是因为您基本上 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 未观察到的异常会使您的进程崩溃。

更多详情:Task Exception Handling in .NET 4.5

【讨论】:

【参考方案10】:
var dn = await Task.WhenAll<dynamic>(FeedCat(),SellHouse(),BuyCar());

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

var ct = (Cat)dn[0];

这做起来很简单,用起来也很有用,没有必要去追求复杂的解决方案。

【讨论】:

这里只有一个问题:dynamic 是魔鬼。它用于棘手的 COM 互操作等,不应在任何非绝对需要的情况下使用。特别是如果您关心性能。或键入安全性。或者重构。或者调试。【参考方案11】:

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!

【讨论】:

以上是关于等待具有不同结果的多个任务的主要内容,如果未能解决你的问题,请参考以下文章

面试题 | 等待多个并发结果有哪几种方法?

Dart Future.wait 等待多个期货并返回不同类型的结果

Java多线程

等待多个 goroutine 的结果

无法在 PHP 中显示具有不同链接的多个结果

(Mongo/Mongoose) 如何处理等待多个查询的结果