【问题标题】:Starting multiple async/await functions at once and handling them separately一次启动多个 async/await 函数并分别处理它们
【发布时间】:2012-10-17 10:19:04
【问题描述】:

如何一次启动多个HttpClient.GetAsync() 请求,并在它们各自的响应返回后立即处理它们?首先我尝试的是:

var response1 = await client.GetAsync("http://example.com/");
var response2 = await client.GetAsync("http://stackoverflow.com/");
HandleExample(response1);
HandleStackoverflow(response2);

当然,它仍然是连续的。所以我尝试同时启动它们:

var task1 = client.GetAsync("http://example.com/");
var task2 = client.GetAsync("http://stackoverflow.com/");
HandleExample(await task1);
HandleStackoverflow(await task2);

现在任务同时启动了,这很好,当然代码还是要一个接一个地等待。

我想要的是能够在“example.com”响应一进来就处理它,而“stackoverflow.com”响应一进来就能够处理。

我可以将这两个任务放在一个数组中,然后在循环中使用Task.WaitAny(),检查哪个任务完成并调用适当的处理程序,但是……这比普通的旧回调有什么好处?或者这不是 async/await 的真正预期用例?如果没有,我将如何将HttpClient.GetAsync() 与回调一起使用?

澄清一下——我所追求的行为类似于这个伪代码:

client.GetAsyncWithCallback("http://example.com/", HandleExample);
client.GetAsyncWithCallback("http://stackoverflow.com/", HandleStackoverflow);

【问题讨论】:

    标签: c# asynchronous windows-8 async-await


    【解决方案1】:

    您可以使用ContinueWithWhenAll等待一个新的Task,task1和task2将并行执行

    var task1 = client.GetAsync("http://example.com/")
                      .ContinueWith(t => HandleExample(t.Result));
    
    var task2 = client.GetAsync("http://stackoverflow.com/")
                      .ContinueWith(t => HandleStackoverflow(t.Result));
    
    var results = await Task.WhenAll(new[] { task1, task2 });
    

    【讨论】:

    • FWIW,因为 Task.WhenAll 在重载中使用参数来获取数组,所以您可以将最后一行更改为 WhenAll(task1, task2) 并让编译器为您构建数组:) @987654321 @
    • 这个答案只是等待他们俩完成。它不会“在它们各自的响应返回后立即处理它们”。
    • [我知道@StephenCleary 的评论真的很老了,但我不想让其他有同样想法的人感到困惑。] 这个答案在各自的ContinueWith 调用中处理它们(其结果是分配给task1task2 的实际任务。等待的WhenAll 只是确保在执行超出它的任何行之前完成这两个“处理”任务。
    【解决方案2】:

    您可以使用一种方法,在它们完成时对其进行重新排序。这是Jon SkeetStephen Toub 描述的一个很好的技巧,我的AsyncEx library 也支持。

    所有三个实现都非常相似。采用我自己的实现:

    /// <summary>
    /// Creates a new array of tasks which complete in order.
    /// </summary>
    /// <typeparam name="T">The type of the results of the tasks.</typeparam>
    /// <param name="tasks">The tasks to order by completion.</param>
    public static Task<T>[] OrderByCompletion<T>(this IEnumerable<Task<T>> tasks)
    {
      // This is a combination of Jon Skeet's approach and Stephen Toub's approach:
      //  http://msmvps.com/blogs/jon_skeet/archive/2012/01/16/eduasync-part-19-ordering-by-completion-ahead-of-time.aspx
      //  http://blogs.msdn.com/b/pfxteam/archive/2012/08/02/processing-tasks-as-they-complete.aspx
    
      // Reify the source task sequence.
      var taskArray = tasks.ToArray();
    
      // Allocate a TCS array and an array of the resulting tasks.
      var numTasks = taskArray.Length;
      var tcs = new TaskCompletionSource<T>[numTasks];
      var ret = new Task<T>[numTasks];
    
      // As each task completes, complete the next tcs.
      int lastIndex = -1;
      Action<Task<T>> continuation = task =>
      {
        var index = Interlocked.Increment(ref lastIndex);
        tcs[index].TryCompleteFromCompletedTask(task);
      };
    
      // Fill out the arrays and attach the continuations.
      for (int i = 0; i != numTasks; ++i)
      {
        tcs[i] = new TaskCompletionSource<T>();
        ret[i] = tcs[i].Task;
        taskArray[i].ContinueWith(continuation, CancellationToken.None, TaskContinuationOptions.ExecuteSynchronously, TaskScheduler.Default);
      }
    
      return ret;
    }
    

    然后你可以这样使用它:

    var tasks = new[]
    {
      client.GetAsync("http://example.com/"),
      client.GetAsync("http://stackoverflow.com/"),
    };
    var orderedTasks = tasks.OrderByCompletion();
    foreach (var task in orderedTasks)
    {
      var response = await task;
      HandleResponse(response);
    }
    

    另一种方法是使用TPL Dataflow;每个任务完成后,将其操作发布到ActionBlock&lt;T&gt;,如下所示:

    var block = new ActionBlock<string>(HandleResponse);
    var tasks = new[]
    {
      client.GetAsync("http://example.com/"),
      client.GetAsync("http://stackoverflow.com/"),
    };
    foreach (var task in tasks)
    {
      task.ContinueWith(t =>
      {
        if (t.IsFaulted)
          ((IDataflowBlock)block).Fault(t.Exception.InnerException);
        else
          block.Post(t.Result);
      });
    }
    

    以上任何一个答案都可以正常工作。如果您的其余代码使用/可以使用 TPL 数据流,那么您可能更喜欢该解决方案。

    【讨论】:

    • Stephen,您能否解释一下为什么多个任务在单个异步方法中等待时会按顺序完成,而不是在它们完成后立即完成?例如foreach (var t in tasks) await t; 并假设 t1t0 之前完成;但t1 结果在t0 最先显示后才会显示。
    • 因为await t1 直到await t0 完成后才会被执行。
    • 啊我想我知道我为什么感到困惑了,但为了清楚起见,所有这些任务仍然是异步执行的,但它们的结果实际上是按顺序处理的,因为每个任务结果都成为前一个任务的延续?也就是说,运行所有这些任务所需的总时间与花费最多时间的单个任务相同,而不是每个任务运行时间的总和。
    • @stt106:所有任务都已在进行中。 await 只是一个“异步等待”。您的示例代码将(异步)等待它们一次完成一个。如果您还有其他问题,请随时提出您自己的问题。
    【解决方案3】:

    声明一个异步函数并传入你的回调:

    void async GetAndHandleAsync(string url, Action<HttpResponseMessage> callback)
    {
        var result = await client.GetAsync(url);
        callback(result);
    }
    

    然后多次调用它:

    GetAndHandleAsync("http://example.com/", HandleExample);
    GetAndHandleAsync("http://stackoverflow.com/", HandleStackoverflow);
    

    【讨论】:

      猜你喜欢
      • 2020-09-22
      • 2020-09-04
      • 2021-04-11
      • 2019-10-15
      • 2015-09-26
      • 2014-03-13
      • 2017-12-17
      相关资源
      最近更新 更多