链接取消令牌

Posted

技术标签:

【中文标题】链接取消令牌【英文标题】:Linking Cancellation Tokens 【发布时间】:2015-06-19 19:36:56 【问题描述】:

我使用传递的取消令牌,以便可以干净地关闭我的服务。该服务具有不断尝试连接到其他服务的逻辑,因此令牌是打破在单独线程中运行的这些重试循环的好方法。我的问题是我需要调用具有内部重试逻辑的服务,但如果重试失败则在设定的时间段后返回。我想创建一个带有超时的新取消令牌,它将为我完成此操作。这样做的问题是我的新令牌没有链接到“主”令牌,所以当主令牌被取消时,我的新令牌仍然有效,直到它超时或建立连接并返回。我想做的是将两个令牌链接在一起,这样当主令牌被取消时,我的新令牌也将取消。我尝试使用CancellationTokenSource.CreateLinkedTokenSource 方法,但是当我的新令牌超时时,它也取消了主令牌。有没有办法用令牌做我需要做的事情,还是需要更改重试逻辑(可能无法轻松做到这一点)

这是我想做的:

主令牌 - 传递各种功能,以便服务可以干净地关闭。 临时令牌 - 传递给单个函数并设置为一分钟后超时

如果主令牌被取消,临时令牌也必须被取消。

当临时令牌到期时,不得取消主令牌。

【问题讨论】:

【参考方案1】:

您想使用CancellationTokenSource.CreateLinkedTokenSource。它允许有一个“父母”和一个“孩子”CancellationTokenSourcees。这是一个简单的例子:

var parentCts = new CancellationTokenSource();
var childCts = CancellationTokenSource.CreateLinkedTokenSource(parentCts.Token);

childCts.CancelAfter(1000);
Console.WriteLine("Cancel child CTS");
Thread.Sleep(2000);
Console.WriteLine("Child CTS: 0", childCts.IsCancellationRequested);
Console.WriteLine("Parent CTS: 0", parentCts.IsCancellationRequested);
Console.WriteLine();

parentCts.Cancel();
Console.WriteLine("Cancel parent CTS");
Console.WriteLine("Child CTS: 0", childCts.IsCancellationRequested);
Console.WriteLine("Parent CTS: 0", parentCts.IsCancellationRequested);

按预期输出:

取消子 CTS 子 CTS:真 父级 CTS:错误

取消父级 CTS 子 CTS:真 父级 CTS:真

【讨论】:

你说的很对。我开始使用 CancellationTokenSource.CreateLinkedTokenSource 但认为它不起作用。我忘记了当令牌超时时会引发异常。这在我的代码中被进一步捕获。这给人的印象是它没有像我预期的那样工作。通过将我的调用放在 try catch 块中,它工作得很好。 @Retrocoder 如果你只想捕获内部标记,我建议使用try doSomething(ct: childCts.Token); catch (OperationCancelledException) when (childCts.IsCancellationRequested) 之类的模式。您可以将其放在重试循环中并在循环内创建子令牌源。然后,当父令牌取消时,它会一直冒泡,但当子令牌取消时,它只是重试。我无法从你的评论中看出——你可能已经正确地做到了;-)。 此外,OperationCancelledException 有自己的CancellationToken 属性,您应该使用catch (ex) when (condition) 方式检查它。这样你就可以确保你得到了正确的取消。 请注意,如果您的代码将许多子令牌源链接到单个父令牌,则应确保处置每个子令牌源以避免内存泄漏。如果您不这样做,则父代将保持所有子代币源处于活动状态,因为它引用了它们中的每一个。在很多情况下,这通常就像写 using var childCts = 一样简单。 @AgentFire 异常的CancellationToken 属性的问题是您的取消令牌的消费者可以制作自己的链接CTS。在这种情况下,即使您的 CTS 导致取消,异常也会说不是您,而是某个未知令牌。您可以做的最好的事情是检查您的 CTS 是否有 IsCancellationRequested,并假设没有独立于您的来源的隐藏取消。 (如果调用者没有要求它,如果有人让OperationCanceledException 传播,我会说这是一个错误。)【参考方案2】:

如果您只有CancellationToken,而不是CancellationTokenSource,那么仍然可以创建链接取消令牌。您只需使用 Register 方法来触发(伪)子的取消:

var child = new CancellationTokenSource();
token.Register(child.Cancel);

您可以使用CancellationTokenSource 做任何您通常会做的事情。例如,您可以在一段时间后取消它,甚至覆盖您之前的令牌。

child.CancelAfter(cancelTime);
token = child.Token;

【讨论】:

@Matt,如果我们将using 用于Register(child.Cancel) 的结果会怎样?像这样:using var _ = cancellationToken.Register(combinedTokenSource.Cancel); @valker 我喜欢使用“使用”语句来处理由 token.Register(child.Cancel) 返回的 CancellationTokenRegistration 的想法。这似乎是防止内存泄漏的最佳方法,如果父令牌是长期存在的并且在父令牌的生命周期内创建了许多子令牌,则会发生这种情况。 (我删除了我原来的评论,因为它无法编辑。) 其实你根本不需要打电话给Register()。相反,只需使用 CancellationTokenSource.CreateLinkedTokenSource(token),如 i3arnon 接受的答案所示【参考方案3】:

作为i3arnon already answered,您可以使用CancellationTokenSource.CreateLinkedTokenSource() 执行此操作。当您想区分取消整体任务与取消子任务而不取消整体任务时,我想尝试展示如何使用此类令牌的模式。

async Task MyAsyncTask(
    CancellationToken ct)

    // Keep retrying until the master process is cancelled.
    while (true)
    
        // Ensure we cancel ourselves if the parent is cancelled.
        ct.ThrowIfCancellationRequested();

        using var childCts = CancellationTokenSource.CreateLinkedTokenSource(ct);
        // Set a timeout because sometimes stuff gets stuck.
        childCts.CancelAfter(TimeSpan.FromSeconds(32));
        try
        
            await DoSomethingAsync(childCts.Token);
        
        // If our attempt timed out, catch so that our retry loop continues.
        // Note: because the token is linked, the parent token may have been
        // cancelled. We check this at the beginning of the while loop.
        catch (OperationCancelledException) when (childCts.IsCancellationRequested)
        
        
    

当临时令牌到期时,不得取消主令牌。

注意MyAsyncTask() 的签名接受CancellationToken 而不是CancellationTokenSource。由于该方法只能访问CancellationToken 上的成员,因此它不会意外取消主/父令牌。我建议您以这样一种方式组织您的代码,即主任务的CancellationTokenSource 对尽可能少的代码可见。在大多数情况下,这可以通过将CancellationTokenSource.Token 传递给方法来完成,而不是共享对CancellationTokenSource 的引用。

我没有调查过,但可能有一种类似反射的方法可以强制取消CancellationToken,而无需访问其CancellationTokenSource。希望这是不可能的,但如果可能的话,这将被视为不好的做法,一般不用担心。

【讨论】:

【参考方案4】:

几个答案提到从父令牌创建链接令牌源。如果您从其他地方获得子令牌,则此模式将失效。相反,您可能希望从您的主令牌和传递给您的方法的令牌创建一个链接的令牌源。

来自微软的文档:https://docs.microsoft.com/en-us/dotnet/standard/threading/how-to-listen-for-multiple-cancellation-requests

public void DoWork(CancellationToken externalToken)

  // Create a new token that combines the internal and external tokens.
  this.internalToken = internalTokenSource.Token;
  this.externalToken = externalToken;

  using (CancellationTokenSource linkedCts =
          CancellationTokenSource.CreateLinkedTokenSource(internalToken, externalToken))
  
      try 
          DoWorkInternal(linkedCts.Token);
      
      catch (OperationCanceledException) when (linkedCts.Token.IsCancellationRequested)
          if (internalToken.IsCancellationRequested) 
              Console.WriteLine("Operation timed out.");
          
          else if (externalToken.IsCancellationRequested) 
              Console.WriteLine("Cancelling per user request.");
              externalToken.ThrowIfCancellationRequested();
          
      
      catch (Exception ex)
      
          //standard error logging here
      
  

通常通过将令牌传递给方法,取消令牌是您可以访问的全部。要使用其他答案的方法,您可能必须重新管道所有其他方法以传递令牌源。此方法允许您只使用令牌。

【讨论】:

此问题的其他答案不需要您“传递令牌源”。 CancellationTokenSource.CreateLinkedTokenSource 方法是静态的,因此可以在任何地方调用它,传递相关的内部或外部令牌。 docs.microsoft.com/en-us/dotnet/api/… @Matt 我花了很长时间才弄清楚去年的柠檬在想什么 - 其他答案使用父令牌和子令牌源(其中大多数生成 childTokenSource 与父标记作为参数)。问题本身是关于链接令牌的,很多用例会要求您链接两个预先存在的令牌,这就是我的回答应该有帮助的地方

以上是关于链接取消令牌的主要内容,如果未能解决你的问题,请参考以下文章

如何使用取消令牌Azure Service Bus(SubscriptionClient)

取消令牌的使用

C#.net 5 使用取消令牌操作下载任务

使用单个取消令牌添加中止所有任务

如何使用取消令牌停止范围服务?

Polly Timeout 乐观使用 HttpClientFactory。如何使用取消令牌?