How to let Task.Delay complete early

Viewed 638

I have a situation where my code is waiting for some time delay:

await Task.Delay(10_000, cancellationToken);

Under some circumstances I want to continue immediately even if the timeout has not yet been expired ("shortcut the delay"). This decision can be taken while the code is already awaiting the Delay.

What I don't want to do: Loop on condition

foreach (var i = 0; i < 10 && !continueImmediately; i++)
{
    await Task.Delay(1000, cancellationToken);
}

because it either has some latency (here 1 second) or wakes up unnecessarily.

What I also don't want to do: Cancel

var linkedTokenSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken)
try
{
   await Task.Delay(1000, linkedTokenSource.Token);
}
catch (OperationCancelledException)
{
   // determine if the delay was cancelled by the cancellationToken or by the delay shortcut...
}

because I want to avoid exceptions.

What I came up with:

TaskCompletionSource tcs = new TaskCompletionSource();
await Task.WhenAny(new[] { tcs.Task, Task.Delay(10_000, cancellationToken) });

and to shortcut the delay: tcs.SetResult().

This seems to work but I an unsure if there is any cleanup missing. E.g. if the shortcut is taken (i.e. the WhenAny completes because of the tcs.Task), will the Task.Delay continue to consume resources?

Note: for sure I like cancellation support but this is not my question, so you may ignore all the cancellationTokens in my question.

3 Answers

You could write your own Delay() which doesn't throw an exception method modelled after the implementation of Task.Delay().

Here's an example which returns true if the cancellation token was cancelled, or false if it was not (and the delay timed-out normally).

// Returns true if cancelled, false if not cancelled.

public static Task<bool> DelayWithoutCancellationException(int delayMilliseconds, CancellationToken cancellationToken)
{
    var tcs = new TaskCompletionSource<bool>();
    var ctr = default(CancellationTokenRegistration);

    Timer timer = null;

    timer = new Timer(_ =>
    {
        ctr.Dispose();
        timer.Dispose();
        tcs.TrySetResult(false);
    }, null, Timeout.Infinite, Timeout.Infinite);

    ctr = cancellationToken.Register(() =>
    {
        timer.Dispose();
        tcs.TrySetResult(true);
    });

    timer.Change(delayMilliseconds, Timeout.Infinite);

    return tcs.Task;
}

Then you can use a composite cancellation source to have two ways to cancel it:

using System;
using System.Diagnostics;
using System.Threading;
using System.Threading.Tasks;

namespace MultitargetedConsole
{
    class Program
    {
        static async Task Main()
        {
            await test();
        }

        static async Task test()
        {
            const int CANCEL1_TIMEOUT =  5000;
            const int CANCEL2_TIMEOUT =  2000;
            const int DELAY_TIMEOUT   = 10000;
            using var tokenSource1 = new CancellationTokenSource(CANCEL1_TIMEOUT);
            using var tokenSource2 = new CancellationTokenSource();
            using var compositeTokenSource = CancellationTokenSource.CreateLinkedTokenSource(tokenSource1.Token, tokenSource2.Token);

            var compositeToken = compositeTokenSource.Token;

            var _ = Task.Run(() => // Simulate something else cancelling tokenSource2 after 2s
            {
                Thread.Sleep(CANCEL2_TIMEOUT);
                tokenSource2.Cancel();
            });

            var sw = Stopwatch.StartNew();

            bool result = await DelayWithoutCancellationException(DELAY_TIMEOUT, compositeToken);
            Console.WriteLine($"Returned {result} after {sw.Elapsed}");
        }
    }
}

This prints something like Returned True after 00:00:02.0132319.

If you change the timeouts like so:

const int CANCEL1_TIMEOUT =  5000;
const int CANCEL2_TIMEOUT =  3000;
const int DELAY_TIMEOUT   =  2000;

The result will be something like Returned False after 00:00:02.0188434


For reference, here is the source code for Task.Delay().

You could use a custom awaiter that doesn't propagate exceptions:

public struct SuppressException : ICriticalNotifyCompletion
{
    private Task _task;
    private bool _continueOnCapturedContext;

    /// <summary>
    /// Returns an awaiter that doesn't propagate the exception or cancellation
    /// of the task. The awaiter's result is true if the task completes
    /// successfully; otherwise, false.
    /// </summary>
    public static SuppressException Await(Task task,
        bool continueOnCapturedContext = true) => new SuppressException
        { _task = task, _continueOnCapturedContext = continueOnCapturedContext };

    public SuppressException GetAwaiter() => this;
    public bool IsCompleted => _task.IsCompleted;
    public void OnCompleted(Action action) => _task.ConfigureAwait(
        _continueOnCapturedContext).GetAwaiter().OnCompleted(action);
    public void UnsafeOnCompleted(Action action) => _task.ConfigureAwait(
        _continueOnCapturedContext).GetAwaiter().UnsafeOnCompleted(action);
    public bool GetResult() => _task.Status == TaskStatus.RanToCompletion;
}

Notice that the GetResult method contains no code that could throw an exception.

Usage example:

var linkedCTS = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);

var task = Task.Delay(1000, linkedCTS.Token);

if (await SuppressException.Await(task))
{
    // The delay was not canceled
}
else if (cancellationToken.IsCancellationRequested)
{
    // The delay was canceled due to the cancellationToken
}
else
{
    // The delay was canceled due to the shortcut
}

The SuppressException struct above is a slightly enhanced version of a bare bone implementation, that can be found in this GitHub post (posted by Stephen Toub). It can also be found in the 1st revision of this answer.

You could always roll your own delay method:

public class DelayTask
{
    private Timer timer;
    private TaskCompletionSource<bool> tcs = new TaskCompletionSource<bool>();
    private CancellationTokenRegistration registration;
    private int lockObj = 0;

    private DelayTask(TimeSpan delay, CancellationToken cancel)
    {
        timer = new Timer(OnElapsed, null, delay, Timeout.InfiniteTimeSpan);
        registration = cancel.Register(OnCancel);
    }

    public static Task Delay(TimeSpan delay, CancellationToken cancel) => new DelayTask(delay, cancel).tcs.Task;

    private void OnCancel() => SetResult(false);
    private void OnElapsed(object state) => SetResult(true);
    private void SetResult( bool completed)
    {
        if (Interlocked.Exchange(ref lockObj, 1) == 0)
        {
            tcs.SetResult(completed);
            timer.Dispose();
            registration.Dispose();
        }
    }
}

As far as I can tell this does essentially the same thing as Task.Delay but return a bool if it completed or not. Note that it is completely untested.

This seems to work but I an unsure if there is any cleanup missing. E.g. if the shortcut is taken (i.e. the WhenAny completes because of the tcs.Task), will the Task.Delay continue to consume resources?

Probably not, See Do I need to dispose of Tasks. In most cases tasks do not need to be disposed. At worst, they will cause some performance penalty due to the need for finalization. Even if Task.Delay was triggered at some later time, the performance impact should be minimal.

Related