在取消和处理 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?的主要内容,如果未能解决你的问题,请参考以下文章