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);
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 });
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.
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);
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With