如何让Task.Delay早点完成

How to let Task.Delay complete early

我的代码正在等待一些时间延迟:

await Task.Delay(10_000, cancellationToken);

在某些情况下,我想立即继续,即使超时尚未到期(“快捷方式延迟”)。可以在代码已经在等待延迟时做出此决定。

我不想做的事情:循环条件

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

因为它有一些延迟(此处为 1 秒)或不必要地唤醒。

我也不想做的事:取消

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

因为我想避免异常。

我得出的结论:

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

并缩短延迟时间:tcs.SetResult()

这似乎可行,但我不确定是否缺少任何清理工作。例如。如果走捷径(即 WhenAny 因为 tcs.Task 而完成),Task.Delay 会继续消耗资源吗?

注:当然我喜欢取消支持,但这不是我的问题,所以你可以忽略我问题中的所有 cancellationTokens。

您可以使用不传播异常的自定义 awaiter

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;
}

请注意,GetResult 方法不包含可能引发异常的代码。

用法示例:

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
}

上面的 SuppressException 结构是基本实现的略微增强版本,可以在这个答案的 this GitHub post (posted by Stephen Toub). It can also be found in the 1st revision 中找到。

您可以随时推出自己的延迟方法:

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();
        }
    }
}

据我所知,这与 Task.Delay 的作用基本相同,但 return 如果完成与否,则为布尔值。请注意,它完全未经测试。

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?

可能不会,参见 Do I need to dispose of Tasks。在大多数情况下,不需要处理任务。在最坏的情况下,由于需要最终确定,它们会导致一些性能损失。即使稍后触发 Task.Delay,对性能的影响也应该是最小的。

您可以编写自己的 Delay(),它不会抛出以 Task.Delay() 的实现为模型的异常方法。

这是一个示例,其中 returns true 如果取消令牌被取消,或者 false 如果没有取消(并且延迟正常超时)。

// 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;
}

那么你可以使用复合取消源来有两种方式取消它:

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}");
        }
    }
}

这会打印类似 Returned True after 00:00:02.0132319 的内容。

如果您像这样更改超时:

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

结果将类似于 Returned False after 00:00:02.0188434


供参考,here is the source codeTask.Delay()