在这种情况下,单个 CancellationTokenSource 如何适用于所有方法?

Posted

技术标签:

【中文标题】在这种情况下,单个 CancellationTokenSource 如何适用于所有方法?【英文标题】:How can a single CancellationTokenSource work for all methods in this scenario? 【发布时间】:2021-06-25 06:44:22 【问题描述】:

我有一个 GUI 应用程序,其中断开按钮需要取消异步任务并关闭端口。 到目前为止,我可以通过在每种方法中使用 CancellationTokenSource 来实现。端口只应在任务取消后关闭,因此首先取消令牌,然后在方法中捕获 OperationCanceledException,进而关闭端口。

我在开头声明:

private CancellationTokenSource my_cancelationTokenSource;
private CancellationTokenSource my_cancelationTokenSource_2;
private CancellationTokenSource my_cancelationTokenSource_3;

那么作为一个例子我有以下三个方法MyMethodAsync、MySecondMethodAsync和MyThirdMethodAsync:

private async Task MyMethodAsync(port)

    byte[] sent;

    my_cancelationTokenSource = new CancellationTokenSource();

    try
    
        do
        
            await Task.Delay(period, my_cancelationTokenSource.Token);
            //some code here..
            byte[] received = await message.SendReceive(sent, my_cancelationTokenSource.Token);
            //some code here..

        
        while (true);
    

    catch (OperationCanceledException)
    
        try
        
            my_cancelationTokenSource.Dispose();
            my_cancelationTokenSource = null;

            if (port.IsOpen)
            
                port.Close();
            
        
        catch  
    



private async Task MySecondMethodAsync(byte[] set)


    my_cancelationTokenSource_2 = new CancellationTokenSource();
   
   try
    
            await Task.Delay(period, my_cancelationTokenSource_2.Token);
            //some code here
            bool check = await MyThirdMethod(set);
            //some code here
        
    
    catch (OperationCanceledException)
    
        try
        
            my_cancelationTokenSource_2.Dispose();
            my_cancelationTokenSource_2 = null;

            if (port.IsOpen)
            
                port.Close();
            
        
        catch  
    


private async Task MyThirdMethodAsync(byte[] set)


    my_cancelationTokenSource_3 = new CancellationTokenSource();
   
   try
    
            await Task.Delay(period, my_cancelationTokenSource_3.Token);
            //some code here
            bool check = await MyThirdMethod(set);
            //some code here
        
    
    catch (OperationCanceledException)
    
        try
        
            my_cancelationTokenSource_3.Dispose();
            my_cancelationTokenSource_3 = null;

            if (port.IsOpen)
            
                port.Close();
            
        
        catch  
    

这里是断开按钮事件:

private void Button_Disconnect_Click(object sender, RoutedEventArgs e)


    my_cancelationTokenSource?.Cancel();
    my_cancelationTokenSource_2?.Cancel();
    my_cancelationTokenSource_3?.Cancel();


我将拥有数十个这样的方法,并且通过这种方式,我必须为每个方法声明 CancellationTokenSource。如何修改代码以使所有方法的一个取消令牌都适用于这种情况?只需为所有方法声明和使用 my_cancelationTokenSource。

【问题讨论】:

那么,使用单个 CTS 吗?您可能希望在连接形成(并且断开连接按钮变为活动状态)时实例化它,并且您希望避免在每个方法中处理它(甚至可能将其设置为 null) 我无法通过使用单个 cancelationTokenSource 来实现它。 您并没有真正解释为什么单个 CTS 对您不起作用,很难评论什么是好的解决方案。然而,作为一个在黑暗中的镜头,您有能力创建链接的 CTS(因此您可以拥有 1 个主控,该主控将取消所有子级以及为每个单独的方法保留一个子级,保留取消特定方法而不取消其余方法的功能)。看看CancellationTokenSource.CreateLinkedTokenSource(docs.microsoft.com/en-us/dotnet/api/…) @pnatk 我们很难帮助解决“无法管理”的问题。您能否发布您尝试过的代码,以及它不起作用的原因? @Knoop 我的问题是我不知道如何正确实现它。这是一个我有点害怕的地方。 【参考方案1】:

使用单个 CancellationTokenSource 并将 token 传递给每个方法。无论如何,您应该在任何异常之后清理端口,而不仅仅是取消,所以 finally 就可以了。

您首先需要处理/忽略OperationCancelledException 调用这些方法的任何内容。 (或者如果调用者不需要知道取消,你可以在函数内吃掉它——如果不知道它们是如何被调用的,很难给出建议)

private async Task MyMethodAsync(port, CancellationToken cancelToken)

    byte[] sent;

    try
    
        do
        
            await Task.Delay(period, cancelToken);
            //some code here..
            byte[] received = await message.SendReceive(sent, cancelToken);
            //some code here..

        
        while (true);
    
    finally
    
        try
        
            if (port.IsOpen)
            
                port.Close();
            
        
        catch  
    



private void Button_Disconnect_Click(object sender, RoutedEventArgs e)

    my_cancelationTokenSource.Cancel();
    my_cancelationTokenSource.Dispose();

【讨论】:

在这种情况下,我需要在一开始就这样声明为全局变量?:CancellationTokenSource my_cancelationTokenSource = new CancellationTokenSource(); 是的,如果它只在关机时取消,但我认为每次连接时创建它是否有意义? 我明白你的意思是声明为全局变量但在连接时创建? 您也没有处理和清空 TokenSource。是故意的吗? 是的,成员变量(不是全局变量),在需要时创建,但是在不知道您的应用程序流程的情况下很难给出建议是的,您应该处理源代码,我的错误在那里。如果您每次需要它时都重新创建它,则不需要归零。【参考方案2】:

我会有几十个这样的方法,通过这种方式我必须为每个方法声明 CancellationTokenSource

我不太确定操作取消令牌的最佳方法是什么,但这听起来更像是 OOP 问题。您可以使用类来重用共享逻辑,而不是让多个方法共享相同的逻辑。例如,您可以有一个基类声明CancellationTokenSource 的某些受保护字段、将使用该字段的公共方法和将执行特定工作的抽象方法。

    public abstract class MyAsyncWorkerBase
    
        protected CancellationTokenSource _cancellationTokenSource = new CancellationTokenSource();

        public void Cancel()
        
            _cancellationTokenSource.Cancel();
        

        public async Task DoWork(IDontKnowTheType port)
        
            try
            
                await DoMySpecificWork(port);
            
            catch (OperationCanceledException)
            
                try
                
                    _cancellationTokenSource.Dispose();
                    _cancellationTokenSource = null;

                    if (port.IsOpen)
                    
                        port.Close();
                    
                
                catch  
            
        

        protected abstract Task DoMySpecificWork(IDontKnowTheType port);
    

然后你可以继承你的基类,然后在DoMySpecificWork 中做你的事情。 我不知道你的代码库所以我的例子有点模糊,你当然可以改进它。您还可以添加一些代码来处理多次调用DoWork。 最后,为方便起见,您可以将所有 MyAsyncWorkerBase 放入某个数组中,然后在 Button_Disconnect_Click 中循环遍历它们并在每个数组上调用 Cancel

【讨论】:

谢谢。您能否还添加在方法中调用/使用它的方式? 抱歉,不确定你想在方法中调用什么。如果你问如何覆盖 DoMySpecificWork 应该如何工作 - 你只需将你的代码放在那里并将 _cancellationTokenSource 传递到你需要它的地方(例如message.SendReceive)如果你问如何使用类本身 - 很难说因为我不知道如何其余代码看起来像 =) 最简单的方法 - 您可以在开始时创建所有实例,然后在按钮处理程序中调用它们(我假设,因为它的 GUI 应用程序)就像 await myWorker.DoWork(port) 我的意思是您可以使用我的一种方法 MySecondMethod 作为示例来展示它是如何在那里使用的。你的方式很有趣,但我只需要确定实施。【参考方案3】:

您可以对所有方法使用单个 CancellationTokenSource。要关闭端口,您可以简单地 register 取消令牌上的回调,当令牌源被取消时将收到通知。

_cancellationTokenSource.Token.Register(() => 
                               
                                   if(port.IsOpen)
                                   
                                       port.Close();
                                   
                               );

【讨论】:

如果 (port.IsOpen) port.Close(); ?您能否更明确地添加它的位置? 你没有指定port的范围,我想它是一个类上的一个字段,所以它将替换每个方法上的代码。每当取消令牌源被取消时,它都会关闭端口,因此您可以将此代码放在创建端口的位置(如果您已经可以访问取消令牌)或创建取消令牌源时。 您忘记注销了。这段代码是内存不足异常的潜在坑。它还需要 trycatch 块,因为未处理的异常会使您的应用死机。 @eocron 注册回调由取消令牌源保存,并在取消或处置 cts 后清理。风险在于回调指向比 cts 寿命更长的引用并且 cts 未被清除。关于异常,我们确实可以包装一个try/catch,因为当cts被取消时,回调会同步执行,但我认为这不是不使用它的理由,事件处理程序也是如此。可以使用应用级别的全局处理程序。 我认为,您应该在回答中指出这一点。

以上是关于在这种情况下,单个 CancellationTokenSource 如何适用于所有方法?的主要内容,如果未能解决你的问题,请参考以下文章

在没有提示 TFS 命令行的情况下获取单个工作区

在这种情况下如何避免死锁?

如何在 acrobat 中链接表单域,使其表现为单个域

单个记录查找的 Spark 性能

sbt 中的 ScalaTest:有没有办法在没有标签的情况下运行单个测试?

如何设置单个表格单元格填充?