Asynchronous Task.WhenAll with timeout
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;
}
}
broersa
Updated on July 08, 2022Comments
-
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 about 12 yearsI 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 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 about 12 years@broersa First, I think you got that wrong, the relation between threads and
Task
s orasync
methods is not that simple. Second, why would that matter? -
broersa about 12 years@svick Blocking is the word I sought. Things are getting clear now.
-
svick over 11 yearsDo you want to add a summary of what it says?
-
David Peden over 11 yearsNot 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 almost 11 yearsThis has the most upvotes, but do we know if this is now a valid approach for accomplishing this?
-
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 over 9 yearsAlthough 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 over 9 yearsThis looks like something that should be encapsulated into a helper method.
-
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 over 9 yearsa) The link is broken. b) This works for a single task, which isn't what the OP asked about.
-
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 over 9 years@I3arnon - Nice!. I like it.
-
Gertjan almost 8 yearsThe 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 over 7 yearsIt doesn't fetch the completed sources in case of timeout
-
svick over 7 years@MenelaosVergis That's what the second part of the answer (added by @usr) is for.
-
Menelaos Vergis over 7 yearsThere are two WhenAll, is there any performance issue? The second WhenAll is to unbox a Task< > ? Can you please explain this?
-
i3arnon over 7 years@MenelaosVergis The first
Task.WhenAll
is performed on tasks that return completed tasks (i.e. the results ofTask.WhenAny
s). Then I filter these task with a where clause. Finally I useTask.WhenAll
on these tasks to extract their actual results. All these task should already be completed at this point. -
James South almost 6 yearsCan someone please add a more complete code sample combining both snippet parts? At present it's unclear how it all goes together.
-
Stephan Steiner over 4 yearsThere'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 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 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 over 3 yearsThanks. I've updated the link which is the third article under the same root documentation that you linked.
-
Theodor Zoulias over 2 yearsI would suggest configuring the
ContinueWith
with theTaskScheduler.Default
as argument, to avoid running the continuations on any wacky ambientTaskScheduler.Current
that might be currently active. For example a UITaskScheduler
, or aLowPriorityTaskScheduler
. -
Theodor Zoulias over 2 years
-
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 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 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 over 2 yearsTony 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 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 callTask.Dispose
.