Asynchronous Task.WhenAll with timeout

26,725

Solution 1

You could combine the resulting Task with a Task.Delay() using Task.WhenAny():

await Task.WhenAny(Task.WhenAll(tasks), Task.Delay(timeout));

If you want to harvest completed tasks in case of a timeout:

var completedResults =
  tasks
  .Where(t => t.Status == TaskStatus.RanToCompletion)
  .Select(t => t.Result)
  .ToList();

Solution 2

I think a clearer, more robust option that also does exception handling right would be to use Task.WhenAny on each task together with a timeout task, go through all the completed tasks and filter out the timeout ones, and use await Task.WhenAll() instead of Task.Result to gather all the results.

Here's a complete working solution:

static async Task<TResult[]> WhenAll<TResult>(IEnumerable<Task<TResult>> tasks, TimeSpan timeout)
{
    var timeoutTask = Task.Delay(timeout).ContinueWith(_ => default(TResult));
    var completedTasks = 
        (await Task.WhenAll(tasks.Select(task => Task.WhenAny(task, timeoutTask)))).
        Where(task => task != timeoutTask);
    return await Task.WhenAll(completedTasks);
}

Solution 3

Check out the "Early Bailout" and "Task.Delay" sections from Microsoft's Consuming the Task-based Asynchronous Pattern.

Early bailout. An operation represented by t1 can be grouped in a WhenAny with another task t2, and we can wait on the WhenAny task. t2 could represent a timeout, or cancellation, or some other signal that will cause the WhenAny task to complete prior to t1 completing.

Solution 4

What you describe seems like a very common demand however I could not find anywhere an example of this. And I searched a lot... I finally created the following:

TimeSpan timeout = TimeSpan.FromSeconds(5.0);

Task<Task>[] tasksOfTasks =
{
    Task.WhenAny(SomeTaskAsync("a"), Task.Delay(timeout)),
    Task.WhenAny(SomeTaskAsync("b"), Task.Delay(timeout)),
    Task.WhenAny(SomeTaskAsync("c"), Task.Delay(timeout))
};

Task[] completedTasks = await Task.WhenAll(tasksOfTasks);

List<MyResult> = completedTasks.OfType<Task<MyResult>>().Select(task => task.Result).ToList();

I assume here a method SomeTaskAsync that returns Task<MyResult>.

From the members of completedTasks, only tasks of type MyResult are our own tasks that managed to beat the clock. Task.Delay returns a different type. This requires some compromise on typing, but still works beautifully and quite simple.

(The array can of course be built dynamically using a query + ToArray).

  • Note that this implementation does not require SomeTaskAsync to receive a cancellation token.

Solution 5

In addition to timeout, I also check the cancellation which is useful if you are building a web app.

public static async Task WhenAll(
    IEnumerable<Task> tasks, 
    int millisecondsTimeOut,
    CancellationToken cancellationToken)
{
    using(Task timeoutTask = Task.Delay(millisecondsTimeOut))
    using(Task cancellationMonitorTask = Task.Delay(-1, cancellationToken))
    {
        Task completedTask = await Task.WhenAny(
            Task.WhenAll(tasks), 
            timeoutTask, 
            cancellationMonitorTask
        );

        if (completedTask == timeoutTask)
        {
            throw new TimeoutException();
        }
        if (completedTask == cancellationMonitorTask)
        {
            throw new OperationCanceledException();
        }
        await completedTask;
    }
}
Share:
26,725
broersa
Author by

broersa

Updated on July 08, 2022

Comments

  • broersa
    broersa almost 2 years

    Is there a way in the new async dotnet 4.5 library to set a timeout on the Task.WhenAll method? I want to fetch several sources, and stop after say 5 seconds, and skip the sources that weren't finished.

  • broersa
    broersa about 12 years
    I think these tasks are all started in there own threads and the new async functions are not, but correct me if I'm wrong. I'm just starting this new async stuff.
  • svick
    svick about 12 years
    Task.WaitAll() is blocking, so it's not a good idea to use it in C# 5, if you can avoid it.
  • svick
    svick about 12 years
    @broersa First, I think you got that wrong, the relation between threads and Tasks or async methods is not that simple. Second, why would that matter?
  • broersa
    broersa about 12 years
    @svick Blocking is the word I sought. Things are getting clear now.
  • svick
    svick over 11 years
    Do you want to add a summary of what it says?
  • David Peden
    David Peden over 11 years
    Not sure why you came back to this post but your code sample is exactly what the paper describes (as I assume you are well aware). At your request, I've updated my answer with the verbatim quote.
  • TheJediCowboy
    TheJediCowboy almost 11 years
    This has the most upvotes, but do we know if this is now a valid approach for accomplishing this?
  • svick
    svick almost 11 years
    @CitadelCSAlum What do you mean? This code does what is being asked. If you don't believe me, you can read the documentation or try it yourself.
  • Erez Cohen
    Erez Cohen over 9 years
    Although this is the accepted answer, does it do exactly what was described in the question? If I understand correctly, if the timeout occurs before all the tasks are completed, then no result is received (even if some of the tasks were completed). Am I correct? I was looking for something that will allow extracting results from several tasks - taking only those that beat the timeout, regardless if the rest of tasks failed to do so. See my answer below.
  • svick
    svick over 9 years
    This looks like something that should be encapsulated into a helper method.
  • svick
    svick over 9 years
    @ErezCohen You're right. I guess I answered mostly the title of the question and not the body (especially the "skip the sources that weren't finished" part).
  • i3arnon
    i3arnon over 9 years
    a) The link is broken. b) This works for a single task, which isn't what the OP asked about.
  • i3arnon
    i3arnon over 9 years
    @ErezCohen I've made my answer even simpler, if you want to take a look: stackoverflow.com/a/25733275/885318
  • Erez Cohen
    Erez Cohen over 9 years
    @I3arnon - Nice!. I like it.
  • Gertjan
    Gertjan almost 8 years
    The WhenAny returns the task that is completed. To detect if the delay task is fired or the WhenAll you could also check the result of the await Task.WhenAny.
  • Menelaos Vergis
    Menelaos Vergis over 7 years
    It doesn't fetch the completed sources in case of timeout
  • svick
    svick over 7 years
    @MenelaosVergis That's what the second part of the answer (added by @usr) is for.
  • Menelaos Vergis
    Menelaos Vergis over 7 years
    There are two WhenAll, is there any performance issue? The second WhenAll is to unbox a Task< > ? Can you please explain this?
  • i3arnon
    i3arnon over 7 years
    @MenelaosVergis The first Task.WhenAll is performed on tasks that return completed tasks (i.e. the results of Task.WhenAnys). Then I filter these task with a where clause. Finally I use Task.WhenAll on these tasks to extract their actual results. All these task should already be completed at this point.
  • James South
    James South almost 6 years
    Can someone please add a more complete code sample combining both snippet parts? At present it's unclear how it all goes together.
  • Stephan Steiner
    Stephan Steiner over 4 years
    There's an issue with this code: if any regular tasks complete, the timeout task is not awaited.. so your code will run until the timeout Task is disposed and if that's before the timeout task has run to the end, you'll get an InvalidStateOperation. Leave the tasks standing and you're fine.
  • Theodor Zoulias
    Theodor Zoulias about 4 years
    @James South the two snippets can be combined by just invoking the second after the first. First you await, and then you collect the results of the completed tasks. It is possible that all tasks will be completed, or only same, or none of them.
  • DynaWeb
    DynaWeb over 3 years
    @DavidPeden this link is now broken, google search has brought up this article, not sure if this is the one your referring too. docs.microsoft.com/en-us/dotnet/standard/…
  • David Peden
    David Peden over 3 years
    Thanks. I've updated the link which is the third article under the same root documentation that you linked.
  • Theodor Zoulias
    Theodor Zoulias over 2 years
    I would suggest configuring the ContinueWith with the TaskScheduler.Default as argument, to avoid running the continuations on any wacky ambient TaskScheduler.Current that might be currently active. For example a UI TaskScheduler, or a LowPriorityTaskScheduler.
  • Theodor Zoulias
    Theodor Zoulias over 2 years
  • Panagiotis Kanavos
    Panagiotis Kanavos over 2 years
    @TheodorZoulias on the contrary, the Current scheculer is the best choice when all the continuation does is make a very short call or just return a value. There's no reason to incur the cost of marshalling the call to another thread. What does it matter if it's the UI TaskScheduler or a low priority scheduler when the thread is already active? If anything, askContinuationOptions.ExecuteSynchronously could be used to ensure the same thread is used to avoid rescheduling
  • Panagiotis Kanavos
    Panagiotis Kanavos over 2 years
    @TheodorZoulias I don't think you realize that you're telling one of the Microsoft perf engineers that created the performance guidelines that his code is wrong. I think you've misunderstood that guidance and taken it to extremes without understanding why and when it applies. What does it matter what thread is used to return a constant value? As for your solution, have you considered the complexity and cost of so many wrapped tasks that need unwrapping?
  • Tony
    Tony over 2 years
    @TheodorZoulias You need to dispose the tasks. It is up to the caller to decide how to handle the running tasks if timeout occurs or a cancellation is raised.
  • Theodor Zoulias
    Theodor Zoulias over 2 years
    Tony in the linked article Stephen Toub says "No. Don’t bother disposing of your tasks.". You say "You need to dispose the tasks." I am confused. Whose advice should I follow?
  • Tony
    Tony over 2 years
    @TheodorZoulias I apologize for the confusion. What I meant is it is up to the caller to decide whether the remaining IEnumerable<Task> tasks should continue to run, or should be cancelled/stopped (disposed) them if a cancellation or timeout occurs. I have used the term "dispose" loosely. You don't have to call Task.Dispose.