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


237

我有3个任务:

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

它们都需要先运行,然后我的代码才能继续,我也需要它们的结果。没有结果有什么共同点

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


25
您有订购要求吗?也就是说,您是否要等到猫喂完才卖掉房子?
埃里克·利珀特

Answers:


411

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


83
您可以将其WhenAll完全删除;等待者将确保您在完成所有任务之前不会超过3个以后的任务。
Servy

134
Task.WhenAll()允许以并行模式运行任务。我不明白为什么@Servy建议删除它。没有WhenAll他们,他们将被逐一运作
Sergey G.

87
@Sergey:任务立即开始执行。例如,catTask在从返回时,它已经在运行FeedCat。因此,任何一种方法都行得通-唯一的问题是,您await是一次要一个,还是全部都想。错误处理略有不同-如果使用Task.WhenAll,则await即使它们之一较早失败,也将全部处理。
史蒂芬·克利西

23
@Sergey调用WhenAll对操作的执行时间或执行方式没有影响。它只是有任何可能影响结果如何观察。在这种特定情况下,唯一的区别是前两个方法之一的错误将导致在我的方法中比Stephen的方法更早地在此调用堆栈中引发异常(尽管如果有任何错误,总是会引发相同的错误) )。
Servy 2015年

37
@Sergey:关键是异步方法总是返回“热”(已经启动)任务。
Stephen Cleary

99

await全部启动之后,仅将这三个任务分开。

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

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

8
@Bargitta不,那是错误的。他们将并行工作。随意运行它,自己看看。
2013年

5
人们多年后一直在问同样的问题...我感到很重要的一点是,必须再次强调,答案主体中的任务“ 开始创建 ” :也许他们不打扰阅读评论

9
@StephenYork Task.WhenAll以任何可观察的方式,添加更改几乎不会改变程序的行为。这是一个纯粹的冗余方法调用。如果愿意,欢迎您添加它作为一种美观的选择,但是它不会改变代码的作用。代码的执行时间将是在或不在方法调用相同(当然,技术上也将是一个非常小的调用开销WhenAll,但是这应该是可以忽略不计),才使得该版本稍微长于这个版本来运行。
Servy

4
@StephenYork您的示例按顺序运行操作有两个原因。您的异步方法实际上不是异步的,而是同步的。您拥有总是返回已完成的任务的同步方法的事实阻止了它们同时运行。接下来,您实际上并没有执行启动所有三个异步方法然后依次等待这三个任务的答案。您的示例在前一个方法完成之前不会调用每个方法,因此显式地阻止了一个方法在前一个方法完成之前启动。
Servy

4
@MarcvanNieuwenhuijzen这显然是不正确的,正如此处评论和其他答案中所讨论的那样。添加WhenAll只是纯粹的审美变化。行为上唯一可观察到的差异是,如果较早的任务发生故障,您是否等待较晚的任务完成,通常不需要这样做。如果您不相信有关为什么您的陈述不正确的众多解释,则可以直接为自己运行代码,然后查看结果是否正确。
Servy '18年

37

如果您使用的是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方法。什么名称空间/包?
Yury Scherbakov

@YuryShcherbakov Task.WhenAll()没有返回元组。Result在完成返回的任务之后,将根据提供的任务的属性来构造一个Task.WhenAll()
克里斯·查拉巴鲁克

2
我建议.Result按照斯蒂芬的推理替换电话,以免其他人通过复制您的示例来使不良做法永久化。
julealgon

我想知道为什么这种方法不是框架的这一部分?似乎很有用。他们是否用完了时间并且不得不在单一返回类型上停下来?
伊恩·格兰杰

14

鉴于三个任务- 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品牌没有差别的任务是如何运行(同时,顺序等)。在最高级别上,它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. 喜欢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. 如果可能的话,宁愿IsCompletedSuccessfullyStatus == TaskStatus.RanToCompletion; 现在在.NET Core中存在,Task对于ValueTask<T>


“与这里的各种答案相反,使用await代替Task.WhenAll对任务的运行方式(并发,顺序等)没有影响”,我看不到有任何答案。我已经对他们发表了评论,如果他们这样做的话。关于很多答案的评论很多,但没有答案。你指的是什么?还要注意,您的答案不会处理任务的结果(或处理结果都是不同类型的事实)。您已经用一种方法编写了它们,Task当它们全部完成时只返回一个a ,而不使用结果。
Servy

@Servy你是对的,那是评论;我将使用结果进行调整以显示
Marc Gravell

@Servy调整已添加
Marc Gravell

同样,如果您打算尽早处理同步任务,那么不妨同步处理被取消或出现故障的所有任务,而不仅仅是成功完成的任务。如果您已决定它是程序需要的优化(这很少见,但会发生),那么您不妨一路走下去。
Servy

@Servy是一个复杂的主题-您从两种情况中获得不同的异常语义-​​等待触发异常的行为与访问.Result触发异常的行为不同。在这一点上,国际海事组织(IMO)在await假设例外是罕见但有意义的前提下应该获得“更好的”例外语义
Marc Gravell

12

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

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()部分无用的部分,因为该方法已经执行了?
Kraang Prime

1
@sanuel如果他们返回任务<t>,则不...他们开始异步打开,但不要等待
Reed Copsey

我认为这是不正确的,请参阅@StephenCleary的答案下的讨论...也请参阅Servy的答案。
Rosdi Kasim

1
如果我需要添加.ConfigrtueAwait(false)。我将其添加到Task.WhenAll还是随后的每个等待者中?
AstroSharp

一般而言,@ AstroSharp是一个好主意,将其添加到所有这些对象中(如果第一个完成,则将其有效地忽略),但是在这种情况下,仅执行第一个可能就可以了-除非存在更多异步事情稍后发生。
Reed Copsey

6

如果您试图记录所有错误,请确保在代码中保留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中运行,一旦其中的一些等待完成,它就会失败。发生这种情况的原因是,一旦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中的任务异常处理



2

使用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
Stephen York

1

向前警告

对那些访问此线程以及其他类似线程的人们进行快速了解,他们正在寻找一种使用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);
     }
 }

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

如果要访问Cat,请执行以下操作:

var ct = (Cat)dn[0];

这是非常简单的操作,并且非常有用,不需要复杂的解决方案。


1
唯一的问题dynamic是:魔鬼。它用于棘手的COM互操作等,并且在并非绝对需要的任何情况下都不应使用。特别是如果您关心性能。或输入安全性。或重构。还是调试。
乔尔·穆勒
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.