【问题标题】: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;
}

【问题讨论】:

    标签: c# uwp async-await cancellationtokensource cancellation-token


    【解决方案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”包装器。我相信我发布的代码的最终版本是好的。我的意图是实现一个异步方法,我相信在你的帮助下我成功了。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2015-08-01
    • 2018-01-02
    • 2023-03-16
    • 1970-01-01
    相关资源
    最近更新 更多