在取消和处理 CancellationTokenSource 之间存在延迟是不是可以保证 IsCancellationRequested 将设置为 true?

Posted

技术标签:

【中文标题】在取消和处理 CancellationTokenSource 之间存在延迟是不是可以保证 IsCancellationRequested 将设置为 true?【英文标题】:Will having a delay between canceling and disposing a CancellationTokenSource provide any assurance that IsCancellationRequested will be set to true?在取消和处理 CancellationTokenSource 之间存在延迟是否可以保证 IsCancellationRequested 将设置为 true? 【发布时间】:2021-05-07 01:39:01 【问题描述】:

C# Windows UWP 项目

我在调用另一个方法的异步方法中实现了 CancellationTokenSource 和 CancellationToken。此方法包含一个 while 循环,该循环将 bool 变量的值保持为 true,直到令牌源被取消。

异步方法由鼠标左键按下事件触发,并由使用 ColorPicker 控件时发生的鼠标左键释放事件取消。 bool 变量在 true 时允许将颜色值发送到设备,在 false 时阻止发送。

通过将该值保持为 true,只要鼠标按钮保持按下状态,设备就会在指针围绕颜色选择器移动时连续接收不同的颜色值。释放鼠标按钮后,生成的错误值(由将颜色值发送到设备的例程设置)阻止进一步的颜色消息发送到设备。

我的代码也按照我的意愿行事,但我担心如果我没有正确实现它可能会产生潜在的副作用。我在这个论坛上看到至少一个帖子表明顺序:cancel、dispose 和 set to null 可用于 CancellationTokenSource。但令我担心的是,我有一个可能无穷无尽的 while 循环,它完全取决于接收取消令牌。所以我的问题是过早处理 CancellationTokenSource 是否会阻止 token.IsCanellationRequested 设置为 true,如果是这样,添加延迟是否会增加任何好处?

以下是我的代码中相关的sn-ps:

全局变量:

public static bool colorPickerPtrPressed = false;
static CancellationTokenSource cts = null;
static CancellationToken token;

鼠标按钮事件:

private void ColorPicker_PtrPressedEvent(object sender, PointerRoutedEventArgs e)

    if(cts == null) cts = new CancellationTokenSource();
    token = cts.Token;

    var picker = sender as ColorPicker.ColorPicker;

    colorPickerPtrPressed = true;
    (picker.DataContext as SpheroViewModel).Color = picker.SelectedColor.Color;

    ColorChange(token);



private void ColorPicker_PtrReleasedEvent(object sender, PointerRoutedEventArgs e)

    if (cts != null)
    
        cts.Cancel();
        Task.Delay(500).Wait(); // Allow some time for cancel to take effect
        cts.Dispose();
        cts = null;
     

取消令牌方法:

public static async Task ColorChange(CancellationToken token)

    await Task.Run(() =>
    AllowColorChange(token), token);


public static void AllowColorChange(CancellationToken token)

        while (!token.IsCancellationRequested)
        
            colorPickerPtrPressed = true; // Maintain value as true
            Task.Delay(100).Wait(); // allow color updates periodically
        
    return;

【问题讨论】:

【参考方案1】:

所以我的问题是,在取消和处置 CancellationTokenSource 之间存在延迟,正如我在下面的“ColorPicker_PtrReleasedEvent”中所做的那样,是否可以保证 while 循环将终止?

没有。取消模型是协作的,调用 Cancel 只需通过将所有令牌的 IsCancellationRequested 属性设置为 true 来提供取消通知。

然后由任何可取消的 API(即任何接受 CancellationToken 的方法)来监控此属性的值并响应取消请求。

所以ColorPicker_PtrReleasedEvent 无法保证while 循环将在AllowColorChange 中终止。

【讨论】:

您是说 cts.Cancel() 将始终将 token.IsCancellationRequested 设置为 true(while 循环监控)并且之后的延迟不会增加任何内容? @Stroker347 如果您想确保可取消操作确实在取消状态下完成,则在您Cancel 取消源之后等待或(最好)等待该操作是无可替代的。使用任意延迟是导致应用程序性能不佳和偶尔失败的秘诀。 你的意思是“await cts.Cancel()”还是“await AllowColorChange(token)”? @Stroker347 await AllowColorChange(token)。或者更好的是await _task,其中_task 是之前调用AllowColorChange(token) 的缓存结果。这应该是一个返回Task的异步方法,并且根据guidelines具有Async后缀。和一个更好的名字一般。 AllowColorChange 是适合 bool 属性的名称,而不是方法! 实际上,我会知道while循环是否没有终止,因为释放鼠标按钮后颜色变化会继续发送到我的设备。但是,如果我等待它并检查是否完成,那么如果在释放鼠标按钮时它没有被取消,我可以重新发出 Cancel() 命令。测试表明,即使没有任何延迟,while 循环也会根据需要终止,所以也许我什么都不担心。【参考方案2】:

按照 TZ 的建议,我修改了最后三个方法以等待 AllowColorChange(token) 方法中的取消标志,然后将该结果用作对 CancellationTokenResult 的 dispose() 的许可并将其设置为 null。如果有人发现我所做的事情有问题,请告诉我。以下是修改后的代码,看起来效果不错:

    private void ColorPicker_PntrReleasedEvent(object sender, PointerRoutedEventArgs e)
    
        if (cts != null)
        
            cts.Cancel();
            //Task.Delay(200).Wait(); // Allow some time for cancel to take effect
            //cts.Dispose();
            //cts = null;
        
    

    public static async Task ColorChange(CancellationToken token)
    
        bool task = false;
        task = await Task.Run<bool>(() =>
        AllowColorChange(token), token);
        if (task)
        
            cts.Dispose();
            cts = null;
        
        else
        
            // Shouldn't ever reach this point
            bool isBrkPnt = true;
        
    

    public static async Task<bool> AllowColorChange(CancellationToken token)
    
            while (!token.IsCancellationRequested)
            
                colorPickerPtrPressed = true; // Maintain value as true
                await Task.Delay(100); // allow color updates periodically
            
        return true; // signal that task was canceled
    


【讨论】:

您确定命令cts.Dispose(); cts = null; 将处理已取消的cts,而不是ColorPicker_PtrPressedEvent 处理程序同时创建的新cts?除此之外,您没有将token 传递给Task.Delay 方法,因此取消将被稍微推迟(平均约50 毫秒)。此外,取消的标准做法是传播OperationCanceledException,而不是bool 值。这意味着应该很少使用IsCancellationRequested 属性,而应该使用ThrowIfCancellationRequested 方法。 如果当前一个为空,我只创建一个新的 CancellationTokenSource,所以一次不应该超过一个,所以我认为我在第一点上没问题。我尝试将令牌传递给 Delay 方法,它导致我的 while 循环停止按我想要的方式运行,所以我必须进一步调查。 bool 值是我如何使用返回值的自然选择,但我会研究您建议的更好选择。感谢您的建议,它们非常有帮助,我不再担心我可能会过早地处理 CancellationTokenSource。 TZ,我将 Task ColorChange(token) 的主体修改为:“try await Task.Run(async() => while(true)token.ThrowIfCancellationRequested(); colorPickerPtrPressed = true ; await Task.Delay(100, token);,token; catch (OperationCanceledException ex) when (ex.CancellationToken == token)cts.Dispose(); cts = null;" 这工作并消除了使用的 AllowColorChange 方法。此外,现在可以将令牌传递给 Delay 方法。这些更改是否更符合您的建议? 是的,好多了。我不清楚你需要每 100 毫秒设置一次 colorPickerPtrPressed = true 的原因,我想这不是你想要做的任何事情的最佳解决方案,但它可能已经足够好了。 :-) 每次我向设备发送颜色命令时,我都会在发送命令的方法中设置 colorPickerPtrPressed = false,以防止在我将指针移出颜色选择器控件时进一步更改。但我也希望能够按住鼠标按钮并发送连续的流,直到我得到正确的颜色。当定期更新足够时,我只是不需要或不希望使用颜色更改命令使我的设备过载。再次感谢您的帮助,这很有启发性。【参考方案3】:

在执行 Theodor Zoulias 的建议后,最终代码如下所示。可以看出,在取消和释放 CancellationTokenSource 之间没有使用任意延迟,而是将释放移动到由抛出 token.ThrowIfCancellationRequested(); 导致的 OperationCanceledException 触发的 catch 块;来自 while() 循环,该循环被移动到 try 块中,并且它的测试参数设置为 true。不再需要测试 token.IsCancellationRequested 作为 while 循环的参数的值。这种编码确保在 try 块中的任务被取消之前不会发生处置。

     private void ColorPicker_PntrPressedEvent(object sender, PointerRoutedEventArgs e)
    
        if(cts == null) cts = new CancellationTokenSource();
        token = cts.Token;

        var picker = sender as ColorPicker.ColorPicker;

        colorPickerPtrPressed = true; // True allows new values of color to be sent to device
        (picker.DataContext as SpheroViewModel).Color = picker.SelectedColor.Color;

        ColorChange(token); // Don't await this

    

    private void ColorPicker_PntrReleasedEvent(object sender, PointerRoutedEventArgs e)
    
        if (cts != null)
        
            cts.Cancel();
        
    

    public static async Task ColorChange(CancellationToken token)
    
        try
        
            await Task.Run(async () =>
            
                while (true)
                
                    token.ThrowIfCancellationRequested();
                    colorPickerPtrPressed = true; // Maintain value as true while mouse button remains pressed
                    await Task.Delay(100, token); // allow color updates periodically
                
            , token);
        
        catch (OperationCanceledException ex) when (ex.CancellationToken == token) // includes TaskCanceledException
        
            if (cts != null) // Shouldn't arrive here if it is null but check just in case
            
                try
                
                    cts.Dispose();
                    cts = null;
                
                catch (ObjectDisposedException e)
                
                    // Already disposed, do nothing
                    bool brkPnt = true;
                
            
        
    


【讨论】:

嗯,将循环包装在 Task.Run 中会使事情变得复杂,因为现在并非所有事情都会发生在同一个线程(UI 线程)上。现在有可能在后台线程上调用cts.Dispose 之后,在UI 线程上调用cts.Cancel();,从而产生ObjectDisposedException。我建议删除Task.Run,因为它似乎没有提供任何东西。每 100 毫秒在 UI 线程上调用一个仅更新 bool 变量的延续,无需担心。 顺便说一句,您是否考虑过用简单的System.Windows.Forms.Timer 替换这台机器?您可以在适当的时候切换其Enabled 属性(或调用其Start/Stop 方法)。 好的。在没有 Task.Run 包装器的情况下,一切似乎都可以正常工作。 TZ,仅供参考:经过进一步研究,我发现,在存在 Task.Run 包装器的情况下,CancellationTokenSource 的创建、取消和处置都发生在 UI 线程中。只有 while() 循环在线程池线程中运行。我喜欢这种配置,因为 while() 循环中的“await Task.Delay()”是可选的,如果它不存在,则 while() 循环将阻止所有进一步的执行,除非存在“await Task.Run”包装器。我相信我发布的代码的最终版本是好的。我的意图是实现一个异步方法,我相信在你的帮助下我成功了。

以上是关于在取消和处理 CancellationTokenSource 之间存在延迟是不是可以保证 IsCancellationRequested 将设置为 true?的主要内容,如果未能解决你的问题,请参考以下文章

在 Task.WaitAll 中处理取消的任务和任务异常?

《CLR via C#》之线程处理——协作式取消和超时

使用 Express/Node.js 和 Angular 处理取消的请求

如何在 Xamarin Forms 中处理/取消导航

SylixOS 线程取消处理流程

如何使用 CancellationToken 属性?