Timeout pattern on task-based asynchronous method in C#

13,317

Solution 1

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:

  1. 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.

  2. 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);

Solution 2

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();
}
Share:
13,317
Matías Fidemraizer
Author by

Matías Fidemraizer

I define myself as a passionate software guy. From a young age I have always looked forward to new technologies. I have seen first-hand how the Internet has grown and evolved. From plain hypertext to the internet of services and cloud computing. I grew up along with the Internet, I have been learning Web tech and programming for more than 15 years. I am self-taught but an enthusiast and big fan of Computer Science, this has led me to learn and challenge myself every day. I understand Software as more than just code, it is creativity and innovation and a great opportunity to share knowledge with other professionals. After all, the tool does not make the professional. My principle is "enthusiasts do great things". A tool is just a possible expression of some valid solution to get things done right! What am I looking for? I am committed to grow, share, build, create and innovate. At the end of the day, to learn and enjoy.

Updated on July 17, 2022

Comments

  • Matías Fidemraizer
    Matías Fidemraizer almost 2 years

    As far as I know, there're two possible patterns to implement a timeout to task-based asynchronous methods:

    Built-in timeout

    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...

    No built-in timeout

    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.

    The whole question

    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?