Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Starting multiple async/await functions at once and handling them separately

How do you start multiple HttpClient.GetAsync() requests at once, and handle them each as soon as their respective responses come back? First what I tried is:

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

But of course it's still sequential. So then I tried starting them both at once:

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

Now the tasks are started at the same time, which is good, but of course the code still has to wait for one after the other.

What I want is to be able to handle the "example.com" response as soon as it comes in, and the "stackoverflow.com" response as soon as it comes in.

I could put the two tasks in an array an use Task.WaitAny() in a loop, checking which one finished and call the appropriate handler, but then ... how is that better than just regular old callbacks? Or is this not really an intended use case for async/await? If not, how would I use HttpClient.GetAsync() with callbacks?

To clarify -- the behaviour I'm after is something like this pseudo-code:

client.GetAsyncWithCallback("http://example.com/", HandleExample);
client.GetAsyncWithCallback("http://stackoverflow.com/", HandleStackoverflow);
like image 913
Ben Hoyt Avatar asked Oct 17 '12 10:10

Ben Hoyt


3 Answers

You can use ContinueWith and WhenAll to await one new Task, task1 and task2 will be executed in parallel

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 });
like image 159
cuongle Avatar answered Oct 11 '22 19:10

cuongle


You can use a method that will re-order them as they complete. This is a nice trick described by Jon Skeet and Stephen Toub, and also supported by my AsyncEx library.

All three implementations are very similar. Taking my own implementation:

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

You can then use it as such:

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

Another approach is to use TPL Dataflow; as each task completes, post its operation to an ActionBlock<T>, something like this:

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

Either of the above answers will work fine. If the rest of your code uses / could use TPL Dataflow, then you may prefer that solution.

like image 22
Stephen Cleary Avatar answered Oct 11 '22 18:10

Stephen Cleary


Declare an async function and pass your callback in:

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

And then just call it multiple times:

GetAndHandleAsync("http://example.com/", HandleExample);
GetAndHandleAsync("http://stackoverflow.com/", HandleStackoverflow);
like image 23
Jan Avatar answered Oct 11 '22 18:10

Jan