等待CancellationToken取消请求

本文关键字:请求 取消 CancellationToken 等待 | 更新日期: 2023-09-27 18:12:09

如何暂停执行直到请求取消?

var cts = new CancellationTokenSource();
Task.Run(() =>
{
    // Wait for the Cancel...
    Console.WriteLine("Canceled!");
});
Console.ReadKey();
cts.Cancel();
Console.ReadKey();

等待CancellationToken取消请求

可以使用WaitHandle同步等待:

static void Main()
{
    var cts = new CancellationTokenSource();
    Task.Run(() =>
    {
        // Wait for the Cancel...
        cts.Token.WaitHandle.WaitOne();
        Console.WriteLine("Canceled!");
    });
    Console.ReadKey();
    cts.Cancel();
    Console.ReadKey();
}

也就是说,"阻塞线程直到某些事情被取消"是一个非常不常见的场景,因此您可能使用了错误的工具来执行此任务。如果您需要等待某些事情(不是特定的取消),您可以使用TaskCompletionSource代替。如果你需要对取消做出反应,你可以使用CancellationToken.Register来附加一个回调(因此避免阻塞线程)。

CancellationTokenSource在内部使用ManualResetEvent,您可以等待暴露的WaitHandle暂停执行,直到它被设置。

var cts = new CancellationTokenSource();
Task.Run(() =>
{
    WaitHandle.WaitAny(new[] { cts.Token.WaitHandle });
    Console.WriteLine("Canceled!");
});
Console.ReadKey();
cts.Cancel();
Console.ReadKey();

这是在CancellationTokenSource:

中定义的WaitHandle。
ManualResetEvent mre = new ManualResetEvent(false);
if (Interlocked.CompareExchange(ref m_kernelEvent, mre, null) != null)
{    
    ((IDisposable)mre).Dispose();
}
// There is a ---- between checking IsCancellationRequested and setting the event.
// However, at this point, the kernel object definitely exists and the cases are:
//   1. if IsCancellationRequested = true, then we will call Set()
//   2. if IsCancellationRequested = false, then NotifyCancellation will see that the event exists, and will call Set().
if (IsCancellationRequested)
    m_kernelEvent.Set();
return m_kernelEvent;

并且Token只是从源返回句柄(有一个内部变量引用它)。

另一个选择是注册Token回调并使用您自己的ManualResetEvent:

var cts = new CancellationTokenSource();
Task.Run(() =>
{
    var mre = new ManualResetEvent(false);
    var registration = cts.Token.Register(() => mre.Set());
    using (registration)
    {
        mre.WaitOne();
        Console.WriteLine("Canceled!");
    }
});
Console.ReadKey();
cts.Cancel();
Console.ReadKey();

示例:https://blogs.msdn.microsoft.com/pfxteam/2009/05/22/net-4-cancellation-framework/

最简洁的方式可能是:

try { await Task.Delay(Timeout.Infinite, cts.Token); } catch { }

这种方法依赖于捕获异常,异常是昂贵的。如果您只是偶尔等待令牌,这应该不是问题,但是如果您在紧循环中等待令牌,您可能希望在项目中包含一个便宜的CancellationToken的等待器,如本文所述。它允许你这样做:

await cts.Token;

可选:异常也可以通过以下技巧来抑制:

await Task.WhenAny(Task.Delay(Timeout.Infinite, cts.Token));

使用Task.WhenAny有一些关于TaskScheduler.UnobservedTaskException事件的含义,如本答案所解释的。

此代码可用于等待取消事件而不阻塞线程。

。NET6版本可以非常简单,并且允许取消await

public async Task Run(CancellationToken cancellationToken)
{
    // Simplification for the sake of example
    var cts = new CancellationTokenSource();
    
    var waitForStop = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
    // IHostApplicationLifetime event can be used instead of `cts.Token`
    CancellationTokenRegistration registration = cts.Token.Register(() => waitForStop.SetResult());
    await using var _ = registration.ConfigureAwait(false);
    
    await waitForStop.Task.WaitAsync(cancellationToken).ConfigureAwait(false);
}

TaskCanceledException不会因为cts.Cancel()调用而被抛出。它将允许在离开方法之前做一些事情(例如优雅关闭)。然而,如果取消cancellationToken的请求,它将抛出TaskCanceledException的预期。

如果开发人员需要返回结果:

var cts = new CancellationTokenSource();
var waitForStop = new TaskCompletionSource<bool?>(TaskCreationOptions.RunContinuationsAsynchronously);
cts.Token.Register(obj =>
{
    var tcs = (TaskCompletionSource<bool?>)obj!;
    tcs.TrySetResult(true);
}, waitForStop);
var result = await waitForStop.Task.ConfigureAwait(false);