Awaiting multiple Tasks with different results

后端 未结 10 1396
醉话见心
醉话见心 2020-11-22 12:59

I have 3 tasks:

private async Task FeedCat() {}
private async Task SellHouse() {}
private async Task BuyCar() {}
         


        
相关标签:
10条回答
  • 2020-11-22 13:35

    After you use WhenAll, you can pull the results out individually with 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;
    

    You can also use Task.Result (since you know by this point they have all completed successfully). However, I recommend using await because it's clearly correct, while Result can cause problems in other scenarios.

    0 讨论(0)
  • 2020-11-22 13:35

    In case you are trying to log all errors make sure you keep Task.WhenAll line in your code, lot of comments suggest that you can remove it and wait for individual tasks. Task.WhenAll is really important for error handling. Without this line you potentially leaving your code open for unobserved exceptions.

    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;
    

    Imagine FeedCat throws exception in the following code:

    var catTask = FeedCat();
    var houseTask = SellHouse();
    var carTask = BuyCar();
    
    var cat = await catTask;
    var house = await houseTask;
    var car = await carTask;
    

    In that case you will never await on houseTask nor carTask. There are 3 possible scenarios here:

    1. SellHouse is already completed successfully when FeedCat failed. In this case you are fine.

    2. SellHouse is not complete and fails with exception at some point. Exception is not observed and will be rethrown on finalizer thread.

    3. 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.

    Here is error that you will get for 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()<---
    

    For case (2) you will get similar error but with original exception stack trace.

    For .NET 4.0 and later you can catch unobserved exceptions using TaskScheduler.UnobservedTaskException. For .NET 4.5 and later unobserved exceptions are swallowed by default for .NET 4.0 unobserved exception will crash your process.

    More details here: Task Exception Handling in .NET 4.5

    0 讨论(0)
  • 2020-11-22 13:41

    You can use Task.WhenAll as mentioned, or Task.WaitAll, depending on whether you want the thread to wait. Take a look at the link for an explanation of both.

    WaitAll vs WhenAll

    0 讨论(0)
  • 2020-11-22 13:44

    Given three tasks - FeedCat(), SellHouse() and BuyCar(), there are two interesting cases: either they all complete synchronously (for some reason, perhaps caching or an error), or they don't.

    Let's say we have, from the question:

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

    Now, a simple approach would be:

    Task.WhenAll(x, y, z);
    

    but ... that isn't convenient for processing the results; we'd typically want to await that:

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

    but this does lots of overhead and allocates various arrays (including the params Task[] array) and lists (internally). It works, but it isn't great IMO. In many ways it is simpler to use an async operation and just await each in turn:

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

    Contrary to some of the comments above, using await instead of Task.WhenAll makes no difference to how the tasks run (concurrently, sequentially, etc). At the highest level, Task.WhenAll predates good compiler support for async/await, and was useful when those things didn't exist. It is also useful when you have an arbitrary array of tasks, rather than 3 discreet tasks.

    But: we still have the problem that async/await generates a lot of compiler noise for the continuation. If it is likely that the tasks might actually complete synchronously, then we can optimize this by building in a synchronous path with an asynchronous fallback:

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

    This "sync path with async fallback" approach is increasingly common especially in high performance code where synchronous completions are relatively frequent. Note it won't help at all if the completion is always genuinely asynchronous.

    Additional things that apply here:

    1. 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);
      }
      
    2. 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);
      }
      
    3. if possible, prefer IsCompletedSuccessfully to Status == TaskStatus.RanToCompletion; this now exists in .NET Core for Task, and everywhere for ValueTask<T>

    0 讨论(0)
提交回复
热议问题