如何让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 code 为 Task.Delay()
。
我的代码正在等待一些时间延迟:
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 code 为 Task.Delay()
。