9

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?

Yuval Itzchakov
  • 146,575
  • 32
  • 257
  • 321
Matías Fidemraizer
  • 63,804
  • 18
  • 124
  • 206
  • 1
    Some official guidance: https://learn.microsoft.com/en-us/dotnet/csharp/programming-guide/concepts/async/cancel-async-tasks-after-a-period-of-time – GaTechThomas Aug 11 '21 at 03:10

2 Answers2

14

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();
}
i3arnon
  • 113,022
  • 33
  • 324
  • 344
  • Hey. Actually this is what I was doing before I asked the question without a generalization like your extension method, but you're right that mostly this should work. BTW I'm not sure if controlling flow with exception is a good idea. I wouldn't do that. I would return a tuple with a boolean to specify if the operation was completed or not – Matías Fidemraizer Sep 24 '14 at 08:49
  • 1
    @MatíasFidemraizer Using an exception is the way the .Net library operates. It also throws when there's cancellation. I do prefer to avoid these exceptions but instead of a tuple I return the default value (added to the answer). Another option would by to use the `TryX(out result)` paradigm. – i3arnon Sep 24 '14 at 08:59
  • @i3arnon any place where this is explaining how to implement NetworkStream's ReadAsync and WriteAsync cancellation. I am getting no where, keeps deadlocking on me all the time as per this post https://stackoverflow.com/questions/54816114/c-sharp-setting-writetimeout-readtimeout-on-network-stream-makes-no-difference – cd491415 Feb 26 '19 at 17:47
  • 1
    @cd491415 what do you mean implement? You have your own `NetworkStream`? The one is .NET doesn't override `WriteAsync`/`ReadAsync`, so you get the `Stream` implementation that only looks at the `CancellationToken` if it's cancelled before the operation starts. So, these operations aren't really cancellable. – i3arnon Feb 26 '19 at 23:20
  • @i3arnon No, I am talking about NetworkStream from .NET. I am trying to figure out how to properly use WriteAsync and ReadAsync as I cannot use networking on main thread in Xamarin.Forms. So, I have to use WriteAsync to get info to remote and then if there is response, use ReadAsync to read response I got back. – cd491415 Feb 27 '19 at 00:15
  • Could anyone provide code explaining how to implement timeout cancellation when using NetworkStream and TcpClient in a method that would (a) call TcpClient.Connect(ipAddress, Port), (b) would call NetworkStream.WriteAsync() to send some data to an ip / port, and (c) call ReadAsync() to read response so that all 3 of these have a timeout set on them. So, if either of (a), (b), (c) fails within 3 seconds an OpCancelledException is handled. If any other exception it is also handled. If no exception, the response is processed. And all objects are disposed properly. MS docs dont show this – cd491415 Feb 27 '19 at 21:54
  • @cd491415 https://devblogs.microsoft.com/pfxteam/how-do-i-cancel-non-cancelable-async-operations/ – i3arnon Feb 28 '19 at 07:59
13

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);
Community
  • 1
  • 1
Yuval Itzchakov
  • 146,575
  • 32
  • 257
  • 321
  • This works, though "cancellation" implies that the operation did not (and will not) complete, which is not necessarily true with this code. To quote [Toub](http://blogs.msdn.com/b/pfxteam/archive/2012/10/05/how-do-i-cancel-non-cancelable-async-operations.aspx), this is *not* included in the BCL "out of concern that it would become a crutch too quickly used without thinking through the ramifications, which are subtle." – Stephen Cleary Sep 05 '14 at 12:40
  • I think the OP understands the semantics of "cancellation" being talked about here. Though ill edit my answer to make that clear. – Yuval Itzchakov Sep 05 '14 at 12:56
  • Is there a way to achieve this for multiple tasks (using `Task.WhenAll`)? I have tried it but throwing the exception from one task caused the other tasks to be aborted as well. PS - I need to use `await` rather than `Task.Run` since I need the `HttpContext` in the tasks. – Erez Cohen Sep 07 '14 at 22:55
  • @YuvalItzchakov Does Stephen's Taub article you provided above and written in 2012 (https://blogs.msdn.microsoft.com/pfxteam/2012/10/05/how-do-i-cancel-non-cancelable-async-operations/) really applies still today in 2019? Don't we have a better way now or? Sorry just asking because I am reading about lots of complain when using NetworkStream's ReadAsync and WriteAsync and lots of complains are from that time plust Microsoft documentation on this sucks – cd491415 Feb 26 '19 at 17:41
  • The token does not pass into `DoStuff`.So the inner call does not actually cancel? – joe Mar 27 '19 at 03:18