runtime: Developers can await a Task with a timeout

A very popular StackOverflow Q&A asks how to await a Task with a timeout. A simple Task.WithTimeout(TimeSpan) method would alleviate a lot of time developers spend looking for how to properly do this, and avoid doing it wrong (e.g. leaving timers for finalization).

See how we offer a WithTimeout extension method from vs-threading.

About this issue

  • Original URL
  • State: closed
  • Created 6 years ago
  • Reactions: 15
  • Comments: 35 (33 by maintainers)

Most upvoted comments

I like the ConfigureAwait overload approach, since it more clearly communicates that it is not the underlying task that is being cancelled, but it only concerns this particular awaiter.

It would be great if both timeouts and deadlines could be supported with the same technique. Deadlines being a core part of gRPC, and others.

Thinking about deadlines is interesting. But I’d rather see a single helper added somewhere, e.g. along the lines of:

public static class Timeout
{
    public static TimeSpan FromDeadline(DateTimeOffset deadline)
    {
        TimeSpan timeout = deadline - DateTime.UtcNow;
        return timeout < TimeSpan.Zero ? TimeSpan.Zero : timeout;
    }
    ...
}

rather than add yet another set of overloads to every method throughout the system that takes a timeout. That would be a separate proposal.

TIL IAsyncEnumerable already supports this

Sort of: it has that API, but it does something different.

It’s there because the GetAsyncEnumerator interface accepts a CancellationToken, but if you’re using the language await foreach support, you’re not explicitly calling GetAsyncEnumerator, rather code generated for you is doing so… so this extension method exists to let you pass a token through. And while it could have just been called ConfigureAwait, it doesn’t because it’s not just about adding ConfigureAwait onto awaits, but rather what arguments are passed to GetAsyncEnumerator.

As such, it’s then different from what’s being discussed in this issue: it’s not the equivalent of using the proposed WithCancellation on each await. If, for example, an iterator ignored its [EnumeratorCancellation] argument, the CancellationToken supplied to the WithCancellation would be a nop. In contrast, the one in this proposal would actually cancel the await even as the underlying operation represented by the task may still be in flight.

I think there are several questions that need to be answered before considering a concrete API.

One is key use cases. One of the reasons we added ConfigureAwait as we did was the idea that we might in the future add additional overloads for things exactly like cancellation and timeouts, e.g.

await task.ConfigureAwait(false, cancellationToken);

There are pros and cons to this, in particular on the plus side is we don’t actually need to allocate a Task and are free to do whatever we can internally to make this as efficient as possible (and it arguably makes the semantics clearer, that it’s specifically about the await operation rather than about the task itself). The primary con is it’s not as composable: if you get back a Task, you can use it as you would any other task, in many other situations than you could the awaitable struct returned from ConfigureAwait.

Regarding ValueTask, it’s a middle ground: it gives us a bit more flexibility in how we might optimize the implementation, at the expense of usability; if we expect the overwhelming use case to be await task.WithWhatever, then ValueTask could be the right choice instead of (not in addition to) Task. If we expect it to be something else, then Task is the right answer.

There’s also the question of having both timeouts and cancellation tokens that you want to use together, i.e. should these really be separately named methods, one for cancellation and one for timeout, or should a single API (maybe with overloads) let you utilize both at the same time, ala Task.Delay(int, cancellationToken).

namespace System.Threading.Tasks
{
    public static class TaskTimeoutExtensions
    {
        // throws TaskCanceledException
        public static Task WithCancellation(this Task task, CancellationToken cancellationToken) => throw null;
        public static Task<TResult> WithCancellation<TResult>(this Task<TResult> task, CancellationToken cancellationToken) => throw null;

        // throws TimeoutException
        public static Task WithTimeout(this Task task, TimeSpan timeout) => throw null;
        public static Task<TResult> WithTimeout<TResult>(this Task<TResult> task, TimeSpan timeout) => throw null;

        public static Task WithTimeout(this Task task, int millisecondsTimeout) => throw null;
        public static Task<TResult> WithTimeout<TResult>(this Task<TResult> task, int millisecondsTimeout) => throw null;
    }
}

I haven’t included ValueTask overloads, since it should be straightforward to convert them to task instances.

Eirik, let me know if you decide to modify CA2016’s behavior to exclude ConfigureAwait. I wrote it and I’ll be happy to help.

Here’s what an API for cancellable task awaitables could look like (wlog for the case of Task<T>):

namespace System.Runtime.CompilerServices
{
    public readonly struct ConfiguredCancelableTaskAwaitable<TResult>
    {
        public ConfiguredCancelableTaskAwaitable<TResult>.ConfiguredCancelableTaskAwaiter GetAwaiter();

        public readonly struct ConfiguredCancelableTaskAwaiter : ICriticalNotifyCompletion
        {
            public bool IsCompleted { get; }
            public TResult GetResult();
            public void OnCompleted(Action continuation);
            public void UnsafeOnCompleted(Action continuation);
        }
    }
}

namespace System.Threading.Tasks
{
    public class Task<T>
    {
        public ConfiguredCancelableTaskAwaitable<TResult> ConfigureAwait(bool continueOnCapturedContext, CancellationToken cancellationToken);
    }
}

@stephentoub suggested we incorporate the AwaitBehavior enum proposal into this design. It should be possible to embed it into the existing ConfiguredTaskAwaitable struct without any breaking changes, so assuming we go ahead with it we should also include the following APIs:

namespace System.Threading.Tasks
{
    [Flags]
    public enum ConfigureAwaitBehavior
    {
        NoCapturedContext = 0x1, // equivalent to ConfigureAwait(false)
        NoThrow = 0x2, // when set, no exceptions will be thrown for Faulted/Canceled
        Asynchronous = 0x4, // force the continuation to be asynchronous
        ...
    }

    public class Task<T>
    {
        public ConfiguredTaskAwaitable<TResult> ConfigureAwait(ConfigureAwaitBehavior configureAwaitBehavior);
        public ConfiguredCancelableTaskAwaitable<TResult> ConfigureAwait(ConfigureAwaitBehavior configureAwaitBehavior, CancellationToken cancellationToken);
    }
}

If we are in agreement, I can create a new issue containing an API proposal based on the above sketch, and we can probably close this issue and #22144.

Let’s keep such a proposal separate. I don’t think it should impact this.

Example WithTimeout, WithCancellation. And a third variation that is used to reduce the number of exceptions thrown - WhenCancelled, related API proposal #37505, example use case:

var probeCancellation = cancellation.WhenCancelled();
var probeTask = prober.Probe(SiloAddress, diagnosticProbeNumber);
var task = await Task.WhenAny(stopping, probeCancellation, probeTask);

These implementations involve additional exception throwing/catching because they are built using async/await, but it would be possible to implement them more efficiently so that no exceptions are thrown inside the implementation (only in the consuming code if it uses await).

Me, too. I would, though, like to hear from folks if there are compelling use cases for the WithTimeout/Cancellation methods where the result isn’t immediately awaited.

It should be fairly trivial to write such extension methods when an equivalent ConfigureAwait overload is available.

What is the policy with respect to specifying the timeout as int millisecondsTimeout or as a TimeSpan? It seems to me that if .NET could be done “greenfield” there only should be TimeSpan and never an int. But there’s an argument for keeping consistency with existing APIs.

Has it been decided on how new APIs with a timeout should be designed?

My opinion: Something like await Task.Delay(100) is an anti-pattern. All code should use the TimeSpan form. New APIs should only use TimeSpan because getting rid of that unfortunate timeout style is more important than keeping consistency.

So maybe the int millisecondsTimeout overloads should be removed from this proposal.