深入了解 C# 的 CancellationToken

a6ee1aee16d51018a0167964d6234fef.jpeg

概述:最近,我开始涉足 C# 中的异步编程,同时编写了一个副项目,我看到很多方法使用在其签名中称为的东西。我知道这与取消异步操作有关(名称是一个死赠品,对吧?),但这就是我的知识范围。因此,我深入研究了这个话题,在这里,我展示了我迄今为止所学到的一切的简短版本。CancellationToken请注意,这篇文章并不涉及如何进行异步编程或基于 TAP/任务的异步模式,如果您想复习这些主题,这里有一个很好的参考来自 Microsoft 文档此外,尽管这篇文章确实是特定于 C# 的,但 CancellationToken 的设计非常有趣,这对您来说可能是一本有趣的读物,即使您选择的语言不是 C#,所以请继

最近,我开始涉足 C# 中的异步编程,同时编写了一个副项目,我看到很多方法使用在其签名中称为的东西。我知道这与取消异步操作有关(名称是一个死赠品,对吧?),但这就是我的知识范围。因此,我深入研究了这个话题,在这里,我展示了我迄今为止所学到的一切的简短版本。CancellationToken

请注意,这篇文章并不涉及如何进行异步编程或基于 TAP/任务的异步模式,如果您想复习这些主题,这里有一个很好的参考来自 Microsoft 文档

此外,尽管这篇文章确实是特定于 C# 的,但 CancellationToken 的设计非常有趣,这对您来说可能是一本有趣的读物,即使您选择的语言不是 C#,所以请继续阅读。😊

那么什么是?CancellationToken

显然,异步代码适合长时间运行的操作,并且提供的任务机制非常强大。但有时我们需要控制这个任务的执行流程。为什么?我们希望任务的可观测性,而不是让某些任务占用 CPU 和线程池并占用宝贵的资源。有时,我们希望手动取消任务与因异常(或超时?)而取消任务之间有所区别。

不用担心,.NET 为我们提供了一种基于称为取消令牌的轻量级对象来协作_取消_异步操作的机制。

取消代币的基本心智模型

我们有一个对象,用于创建一个或多个长时间运行的异步操作。此对象会将此令牌传递给所有这些操作。各个操作也可以将此令牌的副本传递给其他操作。稍后,创建令牌的对象可以使用它来请求操作停止它们正在执行的操作,实质上是请求取消。此请求只能由请求对象发出,即任何单个操作都不能使用该令牌取消自身和其他操作。重要的是,每个听众都有责任注意到请求并以适当和及时的方式做出回应。

我知道我们已经有很多文字了,但嘿,别担心。很快,我将添加图片和代码以使其更加清晰,同时还将深入探讨如何执行上面指定的每件事。

.NET 如何实现这一点?

.NET 提供了 2 个类,CancellationTokenSource 和 CancellationToken 来实现取消机制。

  • CancellationTokenSource- 这是负责创建取消令牌并向该令牌的所有副本发送取消请求的对象。

  • CancellationToken- 这是侦听器用来监控令牌当前状态的结构。

还涉及另一种类型,即 。取消令牌的侦听器可以选择抛出此异常,以验证取消的来源,并通知其他人它已响应取消请求。OperationCancelledException

实现上述合作取消模型的一般模式如下:

  • 实例化对象CancellationTokenSource

  • 将属性返回的令牌传递给侦听取消的每个任务或线程CancellationTokenSource.Token

  • 为每个任务或线程提供一种机制来响应此取消

  • 调用该方法以提供取消通知CancellationTokenSource.Cancel

好吧,这涵盖了基础知识,您可以理所当然地跳下车来涉足使用它。但是,如果您愿意,请留下来看看如何执行上述每个步骤,尤其是 3 个步骤,因为有多种方法可以做到这一点。

这是我完全从 Microsoft 文档中窃取的插图,显示了令牌源与其令牌的所有副本之间的关系。 b8abf1fdbaf422c36ecd7f31493b3e49.png

CancellationTokenSource 与其令牌之间的关系。

这里重要的方面是这个模型是合作的,即取消不是强加给听众的。侦听器可以确定如何正常终止以响应取消请求。

此外,源可以使用一个方法调用向令牌的所有副本发出取消请求,这使得使用单个取消令牌取消复杂任务或其子任务变得简单易行。

取消令牌的侦听器还可以通过将多个令牌联接到一个_链接令牌_中来一次侦听多个令牌。

侦听器可以实现各种机制,如轮询、回调或等待句柄,以获得取消通知,从而提供灵活性。

现在让我们看一些代码,看看我们如何使用取消令牌。

使用取消令牌的示例

public async Task CancellableMethod()
{
    var tokenSource = new CancellationTokenSource();
    // Queue some long running tasks
    for(int i = 0;i < 10;++i)
    {
        Task.Run(() => DoSomeWork(tokenSource.Token), tokenSource.Token);
    }
    // After some delay/when you want manual cancellation
    tokenSource.Cancel();
}
// Runs on a different thread
public async Task DoSomeWork(CancellationToken ct)
{
    int maxIterations = 100;
    for(int i = 0;i < maxIterations;++i)
    {
        // Do some long running work
        if(ct.IsCancellationRequested)
        {
            Console.WriteLine("Task cancelled.");
            ct.ThrowIfCancellationRequested();
        }
    }
}

在这里,我们启动可取消的任务,并将取消令牌传递给正在运行的委托。传递到此处的任务是可选的。用户委托通知并响应取消请求。这样,调用线程就不能强行结束任务,只需发出请求取消的信号,委托/任务就可以注意到请求并适当地响应它。

取消令牌用于操作,而不是对象

在此框架中,取消是指操作,而不是对象。这样,一个取消令牌应该引用“可取消操作”。取消令牌中的属性设置为 后,无法再次将其设置为,并且您不能在取消后再次重复使用相同的取消令牌。IsCancellationRequestedtruefalse

如何倾听和回应取消请求

可取消操作或侦听器必须确定如何正常终止以及如何响应取消请求。通常执行一些必需的清理,然后委托立即响应。

但是,在更复杂的情况下,用户委托可能需要通知库代码已发生取消。在这种情况下,终止操作的正确方法是让委托调用 ThrowIfCancellationRequested 方法,这将导致引发 OperationCanceledException。库代码可以在用户委托线程上捕获此异常,并检查异常的令牌,以确定该异常是否表示合作取消或其他异常情况。

Task 类以这种方式处理 OperationCancelledException。(查看现在将取消令牌传递给任务的好处吗?

以下是有关用户委托如何监视取消请求的一些机制

通过轮询收听

对于在循环中实现的长时间运行的操作(如上面的示例所示)或递归方法,侦听器可以通过轮询属性的值来侦听取消请求的值。如果值为 ,则该方法可以执行所需的清理并尽快终止。CancellationToken.IsCancellationRequestedtrue

轮询此属性的最佳频率取决于应用程序,由开发人员确定最佳频率。下面是此方法的一个小示例

public static void SomeLongRunningOperation(CancellationToken ct)
{
    while(!ct.IsCancellationRequested)
    {
        DoWork(); // perform one unit of work
    }
    // perform cleanup if needed
}

这就是很多实现的方式。BackgroundServices

还有另一种变体,我们可以使用该方法抛出适当的 .更喜欢这样做,而不是手动引发此异常。ThrowIfCancellationRequestedOperationCancelledException

public static void SomeLongRunningOperation(CancellationToken ct)
{
    while(true)
    {
        DoWork(); // perform one unit of work
        ct.ThrowIfCancellationRequested(); // this is extremely fast
    }
}

有关此方法的更多详细信息,请参阅此链接。

通过注册回调进行侦听

某些操作可能会被阻止,以至于它们无法及时检查取消令牌的值。对于这些情况,您可以注册一个回调方法,以便在收到取消请求时取消阻止该方法。

该方法用于此目的。它还返回一种对象类型,该对象也可用于注销此回调,无论出于何种原因。RegisterCancellationTokenRegistration

让我们看一个使用这种方法取消 Web 请求的示例:

public static void DownloadSomeHugeFile(CancellationToken ct)
{
    WebClient wc = new WebClient();
    ct.Register(() =>
    {
        wc.CancelAsync();
    });
    // optionally can also store this registration in a variable
    wc.DownloadStringAsync("https://some-download-path");
}

现在,每当源请求取消时,都会调用已注册的回调并发生取消。此注册对象管理线程同步,并确保回调将在精确的时间点停止执行。

请注意,回调方法应该很快,因为它是同步调用的,因此在回调返回之前不会返回调用。Cancel

有关更多详细信息,您可以参考此链接。

还有一种方法是使用等待句柄。但我对他们的了解还不够多,无法写博客介绍它们。因此,如果您对这种方法感兴趣,很抱歉,但这里有 Microsoft 文档参考,可能会为您提供帮助。

同时侦听多个令牌

在某些情况下,侦听器可能必须同时侦听多个取消令牌。例如,除了作为方法参数的参数从外部传入的令牌外,可取消操作可能还必须监视内部取消令牌。为此,请创建一个链接令牌源,该源可以将两个或多个令牌联接到一个令牌中。

下面是一个代码示例:

public void DoWork(CancellationToken ct)
{
    var internalTokenSource = new CancellationTokenSource();
    internalTokenSource.CancelAfter(10000);
    var internalToken = internalTokenSource.Token;
    var externalToken = ct;
    using (CancellationTokenSource linkedCts = CancellationTokenSource.CreateLinkedTokenSource(internalToken, externalToken))
    {
        try
        {
            DoWorkInternal(linkedCts.Token);
        }
        catch(OperationCancelledException)
        {
            if(internalToken.IsCancellationRequested)
            {
                Console.WriteLine("Operation timed out");
            }
            
            if(externalToken.IsCancellationRequested)
            {
                Console.WriteLine("Cancelling per user request.");
            }
        }
    }
}

请注意,当链接令牌引发 OperationCanceledException 时,传递给异常的令牌是链接令牌,而不是任何一个前置令牌。要确定哪些令牌已取消,请直接检查前置令牌的状态。

有关此方法的更多详细信息,请使用此参考。

好吧,这是关于取消代币的很多细节,但我今天感到受到启发。因此,我想通过添加一些推荐的模式来扩展这篇文章。

取消令牌的推荐模式

我希望我对取消代币了解很多,以推荐我自己的一些模式。但在那之前,我将分享这个优秀博客中提到的模式。这是一篇精彩的文章,提供了一些关于如何设计和解决取消令牌的重要指南。

使用取消令牌是一个很好的模式,但支持这些令牌需要一些额外的责任。

1. 知道你什么时候已经过了不可取消的点

如果您已经产生了副作用,并且您的方法不准备在退出时恢复,这将使您处于不一致的状态,请不要取消。因此,如果您已经做了一些工作,并且还有很多工作要做,并且令牌被取消了,则只有在可以取消时才必须取消,从而使对象处于有效状态。这可能意味着您必须完成大量工作,或者撤消之前的所有工作(即还原副作用),或者找到一个方便的位置,您可以在中途停止,但处于有效状态,然后再抛出 OperationCanceledException。换言之,调用方必须能够在取消您的工作后恢复到已知的一致状态,或者意识到取消未得到响应,然后调用方必须决定是接受工作,还是自行恢复其成功完成。

2. 传播您的 CancellationToken

将 CancellationToken 传播到您调用的接受 Token 的所有方法,但在上一点中提到的“不取消点”之后除外。事实上,如果你的方法主要编排对其他方法的调用,这些方法本身接受 CancellationTokens,你可能会发现你个人根本不需要调用 CancellationToken.ThrowIfCancellationRequested(),因为你调用的异步方法通常会为你完成。

3. 完成工作后不要抛出 OperationCancelledException

不要在完成工作后引发 OperationCanceledException,因为令牌已发出信号。返回成功的结果,让调用方决定下一步要做什么。无论如何,呼叫者都不能假设您在给定时间点可以取消,因此即使取消,他们也必须为成功的结果做好准备。

4. 输入验证

输入验证当然可以先于取消检查(因为这有助于突出显示调用代码中的错误)。

5. 考虑根本不检查令牌

如果您的工作非常快,或者将其传播到您调用的方法,请考虑根本不检查令牌。也就是说,调用 CancellationToken.ThrowIfCancellationRequested() 是相当轻量级的,所以除非你在 perf 跟踪上看到它,否则不要想得太难。

取消令牌的可选参数

如果要接受 CancellationToken,但希望将其设为可选,可以使用如下语法执行此操作:

public Task SomethingExpensiveAsync(CancellationToken cancellationToken = default(CancellationToken))
{
  // don't worry about NullReferenceException if the
  // caller omitted the argument because it's a struct.
  cancellationToken.ThrowIfCancellationRequested();
}

最好只在公共 API(如果有)中将 CancellationToken 参数设为可选参数,并将其在其他任何地方保留为必需参数。这确实有助于确保您有意通过调用的所有方法传播 CancellationTokens(上面的 #2)。但当然,请记住,一旦你通过了不取消点,就切换到传递 CancellationToken.None。

将 CancellationToken 保留为方法接受的最后一个参数也是一个很好的 API 模式。无论如何,这与可选参数非常吻合,因为它们必须显示在任何必需的参数之后。

如何处理取消例外

如果您以前遇到过取消,您可能已经注意到以下几种类型的异常:和 。TaskCanceledException 派生自 OperationCanceledException。这意味着,在编写处理已取消操作的后果的捕获块时,应捕获 OperationCanceledException。如果捕获 TaskCanceledException,则可能会让某些取消事件从捕获块中溜走(并可能使应用崩溃)。TaskCanceledExceptionOperationCanceledException

如果您的可取消方法介于其他可取消操作之间,则可能需要在取消时执行清理。这样做时,您可以使用 catch 块,但请务必正确重新抛出:

async Task SendResultAsync(CancellationToken cancellationToken)
{
  try
  {
    await httpClient.SendAsync(form, cancellationToken);
  }
  catch (OperationCanceledException ex)
  {
    // perform your cleanup
    form.Dispose();
    // rethrow exception so caller knows you've canceled.
    // DON'T "throw ex;" because that stomps on
    // the Exception.StackTrace property.
    throw;
  }
}

一些参考资料

如果您想更深入地了解此主题,您可以考虑以下一些参考资料:

  1. 有关取消托管线程的 Microsoft 文档

  2. 取消令牌的推荐模式

  3. Andrew Lock 关于如何在 MVC 控制器中使用取消令牌的精彩帖子

综上所述

这篇文章毫不客气地变得太长了,但它是关于 CancellationTokens 如何工作、如何倾听它们以及使用它时应该做哪些设计注意事项的一个很好的参考。如果您喜欢这种深入探讨,或者觉得它太长且文字化,请在评论中告诉我。如果您也希望我对其他主题进行深入研究,也请关闭声音。

如果你喜欢我的文章,请给我一个赞!谢谢

c52e5696243257b22c5feda388f76b39.gif

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值