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

Ian*_*ink 205 .net c# task-parallel-library async-await .net-4.5

我有3个任务:

private async Task<Cat> FeedCat() {}
private async Task<House> SellHouse() {}
private async Task<Tesla> BuyCar() {}
Run Code Online (Sandbox Code Playgroud)

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

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

Ste*_*ary 349

使用后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;
Run Code Online (Sandbox Code Playgroud)

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

  • `Task.WhenAll()`允许以**并行**模式运行任务.我无法理解为什么@Servy建议删除它.如果没有`WhenAll`,他们将一个接一个地运行 (103认同)
  • 你可以完全删除`WhenAll`; 等待将确保您在完成任务之前不会超过3个以后的任务. (74认同)
  • @Sergey:任务立即开始执行.例如,`catTask`在从FeedCat返回时已经在运行.因此,任何一种方法都可行 - 唯一的问题是你是想要一次"等待"它们还是一起.错误处理略有不同 - 如果你使用`Task.WhenAll`,那么它将"等待"它们全部,即使其中一个提前失败. (74认同)
  • @Sergey:关键是异步方法总是返回"热"(已经开始)的任务. (31认同)
  • @Sergey调用`WhenAll`对操作执行的时间或执行方式没有影响.****具有影响结果如何观察的任何*可能性*.在这种特殊情况下,唯一的区别是前两个方法之一中的错误会导致在我的方法中先于此方法中抛出异常而不是Stephen(尽管如果有任何错误将始终抛出相同的错误) ). (18认同)
  • @Sergey:如果你在调用下一个方法之前等待每一个,那只会是12秒.如果你像Servy的答案一样构造代码,它只会是5秒. (10认同)
  • @Servy:我正在拧人,对不起.我无法想象这样的语法可以**并行**.对我来说绝对令人困惑 (6认同)
  • @Stephen同意你的看法.我想提一下,如果你有3个任务分别为3,4和5秒,`WhenAll`将工作5秒.另一方面,BY CHAIN运行它们将是3 + 4 + 5 = 12秒.正如有人所说:任务不应该与'WhenAll`相互关联.这就是为什么我认为`WhenAll`对性能有重大影响的例子. (5认同)
  • @Vinhent:那些任务不是线程.他们是承诺任务,而不是委派任务.所以首先没有从线程池中取出它们. (5认同)
  • @mindplay.dk `await Task.WhenAll(catTask, houseTask, carTask);` 确保在 `catTask` 失败的情况下,其他两个任务不会变得“即发即弃”。[Fire-and-forget](/sf/ask/4292155311/#61320933) 很糟糕。`await Task.WhenAll` 不是一个可选的奢侈品。这是正确的程序行为的要求。 (5认同)
  • 当我看到一个 `await` 时,我认为有必要等待一个可能未完成的任务返回。在我们的例子中,等待已经发生在 `.WhenAll` 行上,所以结果是同步的。(你不会想调用`.Wait`,只调用`.Result`;请原谅我的错字。) (4认同)
  • @juFo:我的答案仍然是我今天要说的。 (4认同)
  • 在出现故障任务的情况下,“Result”会将原始异常包装在“AggregateException”中,从而使错误处理复杂化。 (3认同)
  • @Sergey如果你不相信Stephen,请继续为自己运行代码并计算时间.你不必接受我们的话. (3认同)
  • @TheodorZoulias,你是对的,我现在明白了 - 使用 `Task.WhenAll` 确保所有任务作为一个整体成功或失败。感谢您删除我的编辑!对于其他难以理解这一点的人,您可以尝试注释掉[此示例](https://dotnetfiddle.net/pZuEyh)中的 `WhenAll` 并查看控制台输出中的差异。 (3认同)
  • 是的,尽管`WhenAll`具有更明确的意图。 (2认同)
  • @Servy 阅读我之前的评论。“按链”运行和“并行”运行是完全不同的。在“WhenAll”中,它们将立即一起运行。没有它,它们将一个接一个地运行。`等待; + 等待;== await.ContinueWith(await;);` (2认同)
  • @AleksanderBethke Stephen和我已经涵盖了同一个评论主题*中两个选项*之间的微小差异.如果这些差异中的任何一个与您相关,则相应地进行选择.如果这些差异在您的情况下都不重要(这是最喜欢的选项),那么这只是个人偏好. (2认同)
  • @codeMonkey:您可以使用`Task.WhenAny`. (2认同)
  • @codeMonkey 或其他任何有此问题的人,请参阅:https://docs.microsoft.com/en-us/dotnet/csharp/programming-guide/concepts/async/start-multiple-async-tasks-and-process-them -当他们完成 (2认同)
  • 这是一个证明不需要 `Task.WhenAll` 的例子。https://gist.github.com/aherrick/239647bf24bd6483924ce6a22f5c8edf (2认同)
  • @aherrick:对。正如我上面解释的那样,如果没有“ WhenAll”,错误处理会有所不同。但是,我通常将其包括在内的主要原因是使代码更加“清晰”。不熟悉它的人更容易理解该方法的意图和语义,因此代码也更易于维护。 (2认同)
  • 当我们在foreach循环中创建任务时,WhenAll非常有用 (2认同)
  • 如果你明确调用`await Task.WhenAll(catTask, houseTask, carTask);`,那么调用`catTask.Result`而不是误导性的`await`不是更好的选择吗?@StephenCleary,正如您所提到的,在`.WhenAll` 完成时不会抛出任何潜在的异常,因此在`await Task.WhenAll` 之后使用`.Wait` 没有害处吗? (2认同)
  • @PatrickSzalapski:使用`await` 的代码对重构更具弹性,因为它不会意外地成为阻塞代码。让我把你的问题转过来。鉴于 `await` 是消耗任务的*自然*方式,与更自然的 `await` 相比,`Result`/`Wait` 有什么优势? (2认同)
  • task.Result是一个线程阻止程序。不要那样做 (2认同)
  • @RichardCollette它不是您知道要完成的任务的线程阻止程序(因为您已经等待Task.WhenAll):“一旦操作的结果可用,它就会被存储并在后续调用Result属性时立即返回”。 (2认同)
  • @juFo 对于它是否与/不与 `WhenAll` 并行运行,从来没有任何疑问。Serj 的假设是有逻辑缺陷的。令人有点不安的是,这么多人投票赞成,却没有坐下来两分钟逻辑地考虑这两种情况下发生的事情。他应该在意识到自己错误后立即删除该评论,以免误导这么多人。 (2认同)

Ser*_*rvy 81

await在启动它们之后,只分别执行三个任务.

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

var cat = await catTask;
var house = await houseTask;
var car = await carTask;
Run Code Online (Sandbox Code Playgroud)

  • @Bargitta不,这是假的.他们将并行完成他们的工作.随意运行它,看看自己. (7认同)
  • @StephenYork以任何可观察的方式添加`Task.WhenAll`几乎没有改变程序的行为.这是一个纯粹的冗余方法调用.如果您愿意,欢迎您添加它作为美学选择,但它不会改变代码的作用.代码的执行时间与是否没有该方法调用是相同的(从技术上来说,调用`WhenAll`会有一个*非常小的开销,但这应该可以忽略不计),只会使该版本*略微*更长运行比这个版本. (6认同)
  • @StephenYork您的示例按顺序运行操作有两个原因.您的异步方法实际上不是异步的,它们是同步的.事实上,您拥有始终返回已完成任务的同步方法,这会阻止它们同时运行.接下来,您实际上并没有执行启动所有三个异步方法的答案中显示的内容,然后*然后依次等待三个任务.您的示例在前一个方法完成之前不调用每个方法,因此明显阻止在上一个完成之前启动它,这与此代码不同. (3认同)
  • @StephenYork 由于您需要提供`FeedCat`、`SellHouse` 和`BuyCar` 的实现来测试代码,您可以对它们中的每一个使用它:`Task FeedCat(){return Task.Delay(1000);}任务 SellHouse(){return Task.Delay(1000);}Task BuyCar(){return Task.Delay(1000);}`。您可以使用这些方法进行测试,然后只需将答案中的代码复制粘贴到一个方法中并运行它以查看它在 1 秒内运行,而不是 3 秒。 (3认同)
  • @MarcvanNieuwenhuijzen这显然不是真的,正如在这里的评论和其他答案中所讨论的那样.添加`WhenAll`是纯粹的审美变化.行为中唯一可观察到的差异是,如果先前的任务发生故障,您是否等待后续任务完成,这通常不需要执行.如果你不相信为什么你的陈述不正确的众多解释,你可以简单地为自己运行代码,看看它不是真的. (3认同)
  • [人民](http://stackoverflow.com/questions/44001825/awaiting-tasks-of-different-types/)几年后一直问同样的问题...我感到再次强调一项任务“ *在[answer](http://stackoverflow.com/a/44002657/6996876)的正文中“从create *开始”:也许他们不用理会评论 (2认同)
  • @Servy根本没有这么说。它说“在启动所有三个任务后,分别等待它们。” 它没有明确说明“创建任务会启动它”。 (2认同)
  • @StephenYork我很高兴您能够运行该示例并获得包含该信息的预期输出,并且您现在了解到,只要使用现在可以使用的异步方法来解决此问题,完全不需要使用“ WhenAll”。 (2认同)
  • @StephenYork 在可执行文件与 IIS 应用程序中,`await` 的工作方式没有本质上的不同。你认为有什么区别是相关的?当然,此答案并非旨在成为涵盖 TPL 所有基础知识的教程。这不仅超出了这个特定问题的范围,而且超出了在 *any* 给出的 SO 答案中真正可以提供的范围。也就是说,如果您有关于此代码为什么或如何工作的更具体的问题,那么请务必提出,我会尽力回答您的问题。 (2认同)
  • @Servy 对不起。我没有很好地看这个例子。我错过了你先开始任务然后等待它们。你是对的。我不能再编辑我的反对票了。它被锁定。但事实上你是对的。 (2认同)

Joe*_*ler 32

如果你正在使用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);
    }
}
Run Code Online (Sandbox Code Playgroud)

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

var (someInt, someString) = await TaskEx.WhenAll(GetIntAsync(), GetStringAsync());
Run Code Online (Sandbox Code Playgroud)

  • 我建议按照Stephen的推理替换`.Result`调用,以免其他人通过复制您的示例而使不良做法永久化。 (2认同)

Ree*_*sey 11

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

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;
Run Code Online (Sandbox Code Playgroud)

  • `var catTask = FeedCat()` 不会执行函数 `FeedCat()` 并将结果存储到 `catTask` 中,从而使 `await Task.WhenAll()` 部分变得无用,因为该方法已经执行了? (2认同)
  • @sanuel 如果他们返回任务 &lt;t&gt;,那么不......他们开始异步打开,但不要等待它 (2认同)
  • 如果我需要添加 .ConfigrtueAwait(false)。我是将它添加到 Task.WhenAll 还是添加到随后的每个等待者? (2认同)
  • @user44它们将并行运行 - 如果没有 WhenAll,您可能会在 house 完成之前得到 Cat 结果等(这可能重要也可能不重要)。 (2认同)

Mar*_*ell 11

给出了三个任务 - FeedCat(),SellHouse()并且BuyCar(),有两个有趣的案例:要么它们都同步完成(出于某种原因,可能是缓存或错误),要么它们没有.

让我们说,从问题:

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

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

Task.WhenAll(x, y, z);
Run Code Online (Sandbox Code Playgroud)

但......处理结果不方便; 我们通常想要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);
}
Run Code Online (Sandbox Code Playgroud)

但这会产生大量开销并分配各种数组(包括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);
}
Run Code Online (Sandbox Code Playgroud)

相反,一些上述的意见,使用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);
}
Run Code Online (Sandbox Code Playgroud)

这种"具有异步回退的同步路径"方​​法越来越普遍,特别是在同步完成相对频繁的高性能代码中.请注意,如果完成始终是真正的异步,它将完全没有帮助.

适用于此处的其他内容:

  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);
    }
    
    Run Code Online (Sandbox Code Playgroud)
  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);
    }
    
    Run Code Online (Sandbox Code Playgroud)
  3. 如果可能的话,宁愿IsCompletedSuccessfullyStatus == TaskStatus.RanToCompletion; 这现在存在于.NET Core中Task,并且随处可见ValueTask<T>

  • @MarcGravell我更仔细地阅读了你的答案,并意识到我误解了你关于任务同步完成的说法。我不明白的是你说的“Task.WhenAll”没有什么区别。但我确实在每次迭代中看到了 `Task.WhenAll` 和 waiting 之间的明显区别。如果我创建 10 个延迟 500 毫秒的等待,并与“Task.WhenAll”一起启动它们,它们将在不到一秒的时间内完成。而如果我等待每 10 个等待 - 它们会按顺序执行(正如我所期望的那样)并在大约 5 秒内完成。 (4认同)

chr*_*dev 6

您可以Task.WhenAll如上所述使用 或Task.WaitAll,具体取决于您是否希望线程等待。查看链接以获取两者的解释。

WaitAll 与 WhenAll


sam*_*mlv 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;
Run Code Online (Sandbox Code Playgroud)

想象一下,FeedCat在以下代码中引发异常:

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

var cat = await catTask;
var house = await houseTask;
var car = await carTask;
Run Code Online (Sandbox Code Playgroud)

在那种情况下,您将永远不会等待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()<---
Run Code Online (Sandbox Code Playgroud)

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

对于.NET 4.0及更高版本,您可以使用TaskScheduler.UnobservedTaskException捕获未观察到的异常。对于.NET 4.5及更高版本,.NET 4.0默认会吞没不可观察的异常,不可观察的异常将使您的进程崩溃。

此处有更多详细信息:.NET 4.5中的任务异常处理

  • 这应该赞成。令我惊讶的是,上面的所有讨论都表明 Task.WhenAll 和 waiting 分别是等效的。它不是。如果任何任务抛出异常,其他任务可能无法完成,或者您可能会丢失其中的错误。由于我的场景涉及所有任务的错误处理以及在抛出错误时重试某些任务,因此在添加 Task.WhenAll 之前,我的代码无法按预期工作。感谢您指出这一点。 (3认同)

归档时间:

查看次数:

76552 次

最近记录:

6 年,1 月 前