Awaiting multiple Tasks with different results

后端 未结 10 1485
醉话见心
醉话见心 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: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 DoTheThings() {
        Task x = FeedCat();
        Task y = SellHouse();
        Task 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 DoTheThings() {
        Task x = FeedCat();
        Task y = SellHouse();
        Task 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 DoTheThings() {
        Task x = FeedCat();
        Task y = SellHouse();
        Task 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 DoTheThings() {
        Task x = FeedCat();
        Task y = SellHouse();
        Task 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 a, Task b, Task 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 DoTheThings() {
          async Task Awaited(Task a, Task b, Task c) {
              return DoWhatever(await a, await b, await c);
          }
          Task x = FeedCat();
          Task y = SellHouse();
          Task 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 to Task if there is a good chance of things ever completely synchronously with many different return values:

      ValueTask DoTheThings() {
          async ValueTask Awaited(ValueTask a, Task b, Task c) {
              return DoWhatever(await a, await b, await c);
          }
          ValueTask x = FeedCat();
          ValueTask y = SellHouse();
          ValueTask z = BuyCar();
      
          if(x.IsCompletedSuccessfully &&
             y.IsCompletedSuccessfully &&
             z.IsCompletedSuccessfully)
              return new ValueTask(
                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

提交回复
热议问题