As far as I know, there're two possible patterns to implement a timeout to task-based asynchronous methods:
public Task DoStuffAsync(TimeSpan timeout)
This approach is harder to implement because it's not easy to implement a global timeout for the entire call stack. For example, a Web API controller receives an HTTP request and it calls DoStuffAsync
, and the caller wants a global timeout of 3 seconds.
That is, each inner async method call will need to receive the subtract of already used time...
public Task DoStuffAsync(CancellationToken cancellationToken)
..........
CancellationTokenSource cancellationSource = new CancellationTokenSource();
Task timeoutTask = Task.Delay(3000);
if(await Task.WhenAny(DoStuffAsync(cancellationTokenSource), timeoutTask) == timeoutTask)
{
cancellationSource.Cancel();
throw new TimeoutException();
}
This seems to be the most reliable and easy to implement pattern. The first caller defines a global timeout, and if it time outs, all pending operations will be cancelled. Also, it provides a cancellation token to the immediate caller and inner calls will share the same cancellation token reference. Thus, if the top caller time outs, it will be able to cancel any working thread.
Is there any pattern that I'm missing or, am I in the right way if I develop APIs using the no built-in timeout?
Is there any pattern that I'm missing or, am I in the right way if I develop APIs using the no built-in timeout?
Disclaimer:
When we talk about a Task
in a cancelled state, we mean that we cancel the operation as it proceeds. This might not be the case here when we talk about cancellation, as we simply discard the task if it completed after the specified interval. This is discussed to extent in Stephan Toubs article below as to why the BCL does not provide OOTB features of cancelling an ongoing operation.
The common approach i see nowadays is the no build-in approach and the one i find myself using mostly to implement a cancelling mechanism. It is definitely the easier of the two, leaving the highest frame to be in charge of cancellation while passing the inner frames the cancellation token. If you find yourself repeating this pattern, you can use the known WithCancellation
extension method:
public static async Task<T> WithCancellation<T>(
this Task<T> task, CancellationToken cancellationToken)
{
var cancellationCompletionSource = new TaskCompletionSource<bool>();
using (cancellationToken.Register(() => cancellationCompletionSource.TrySetResult(true)))
{
if (task != await Task.WhenAny(task, cancellationCompletionSource.Task))
{
throw new OperationCanceledException(cancellationToken);
}
}
return await task;
}
This is from Stephen Toub's How do I cancel non-cancelable async operations? which isn't exactly spot on to what you're asking, but is definitely worth a read.
The Task Cancellation docs go on to specify two ways of task cancellation:
You can terminate the operation by using one of these options:
By simply returning from the delegate. In many scenarios this is sufficient; however, a task instance that is canceled in this way transitions to the TaskStatus.RanToCompletion state, not to the TaskStatus.Canceled state.
By throwing a OperationCanceledException and passing it the token on which cancellation was requested. The preferred way to do this is to use the ThrowIfCancellationRequested method. A task that is canceled in this way transitions to the Canceled state, which the calling code can use to verify that the task responded to its cancellation request
Edit
As for you concern with using a TimeSpan
to specify the desired interval, use the overload of CancellationTokenSource
constructor which takes a TimeSpan
parameter:
var cts = new CancellationTokenSource(TimeSpan.FromSeconds(3));
var task = Task.Run(() => DoStuff()).WithCancellation(cts.Token);
While you can reuse WithCancellation
for both cancellations and timeouts I think it's an overkill for what you need.
A simpler and clearer solution for an async
operation timeout would be to await
both the actual operation and a timeout task using Task.WhenAny
. If the timeout task completes first, you got yourself a timeout. Otherwise, the operation completed successfully:
public static async Task<TResult> WithTimeout<TResult>(this Task<TResult> task, TimeSpan timeout)
{
if (task == await Task.WhenAny(task, Task.Delay(timeout)))
{
return await task;
}
throw new TimeoutException();
}
Usage:
try
{
await DoStuffAsync().WithTimeout(TimeSpan.FromSeconds(5));
}
catch (TimeoutException)
{
// Handle timeout.
}
If you prefer to not throw an exception (as I do) it's even simpler, just return the default value:
public static Task<TResult> WithTimeout<TResult>(this Task<TResult> task, TimeSpan timeout)
{
var timeoutTask = Task.Delay(timeout).ContinueWith(_ => default(TResult), TaskContinuationOptions.ExecuteSynchronously);
return Task.WhenAny(task, timeoutTask).Unwrap();
}
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