[英]CancellationTokenSource for changing state after few seconds
我试图重现人们在 Outlook / Gmail 中看到的效果:打开未读的 email 几秒钟后,它被标记为已读。 但是,如果在该时间过去之前单击不同的 email,则 email 将保持未读状态。
private CancellationTokenSource? _cts;
private async Task OnEmailClicked(int id)
{
if (_cts != null)
{
_cts.Cancel(); // I suspect problem is here
_cts.Token.ThrowIfCancellationRequested(); // or here
_cts.Dispose();
_cts = null;
}
await LoadEmail(id);
try
{
_cts = new CancellationTokenSource();
await Task.Delay(3000, _cts.Token);
}
catch (TaskCanceledException)
{
return;
}
finally {
_cts.Dispose();
_cts = null;
}
await MarkEmailAsRead(id);
}
这给了我奇怪的结果。 有时有效,有时无效。 我不确定在哪里/如何创建、取消和处置令牌源。 显然我的代码是错误的。
实现这一目标的正确方法是什么?
(我不需要“修复”这段代码——我很乐意把它扔掉并以正确的方式进行,如果你能告诉我怎么做的话。我只是把它包含进来以演示我已经尝试过的东西。)
首先,调用ThrowIfCancellationRequested()
会破坏事情。 每次命中该行时都会抛出异常(因为您刚刚取消了该行之前的标记)并且该方法的 rest 将不会运行。
我看到的另一个问题是您在取消后将 _cts 设置为_cts
。 请记住,您将同时使用此方法的两个版本。 假设该方法的两个版本都将在 UI 线程上运行,那么整个代码块将不间断地运行(我已经删除ThrowIfCancellationRequested()
):
if (_cts != null)
{
_cts.Cancel();
_cts.Dispose();
_cts = null;
}
然后,一旦LoadEmail(id)
,该方法的先前不完整版本就有机会运行,它进入catch
并finally
调用_cts.Dispose()
。 但是_cts
已经设置为null
,因此您将获得NullReferenceException
并且MarkEmailAsRead(id)
将永远不会运行。
所以我只会在一个地方而不是两个地方调用Dispose()
。 但我也会保留对我们创建的CancellationTokenSource
的引用,以便如果将新内容分配给_cts
,我们仍然可以在我们创建的那个上调用Dispose()
。 这看起来像这样:
private CancellationTokenSource? _cts;
private async Task OnEmailClicked(int id)
{
_cts?.Cancel();
// Keep a local reference to the token source so that we can still call Dispose()
// on the object we created here even if _cts gets assigned a new object by another
// run of this method
var cts = _cts = new CancellationTokenSource();
await LoadEmail(id);
try
{
await Task.Delay(3000, cts.Token);
}
catch (TaskCanceledException)
{
return;
}
finally {
// Only assign _cts = null if another run hasn't already assigned
// a new token
if (cts == _cts) _cts = null;
cts.Dispose();
}
await MarkEmailAsRead(id);
}
我没有对此进行测试,但看看它是否满足您的要求。
如果cts
与_cts
的使用让您感到困惑,阅读参考类型可能会有所帮助。 但重要的是,将新实例分配给_cts
不会破坏它过去引用的 object,并且仍然引用该 object 的代码的任何其他部分仍然可以使用它。
如果您从未见过?.
之前,它是null-conditional operator 。
据我了解,每次单击不同的 email 时,您基本上都会重新启动看门狗定时器三秒钟。如果 WDT 到期而没有获得新的单击,那么您提前 go 并将最新的 email 标记为“读取”。
多年来,我(成功地)使用相同的方法在为 WDT 启动新任务时取消旧任务。 但后来我意识到我可以这样做:
int _wdtCount = 0;
// Returning 'void' to emphasize that this should 'not' be awaited!
async void onEmailClicked(int id)
{
_wdtCount++;
var capturedCount = _wdtCount;
await Task.Delay(TimeSpan.FromSeconds(3));
// If the 'captured' localCount has not changed after waiting 3 seconds
// it indicates that no new selections have been made in that time.
if(capturedCount.Equals(_wdtCount))
{
await MarkEmailAsRead(id);
}
}
async Task MarkEmailAsRead(int id)
{
Console.WriteLine($"The email with the ID '{id}' has been marked read");
}
无论如何,对我有用,这只是一个想法......
试验台
#region T E S T
onEmailClicked(id: 1);
await Task.Delay(TimeSpan.FromSeconds(1));
onEmailClicked(id: 2);
await Task.Delay(TimeSpan.FromSeconds(1));
onEmailClicked(id: 3);
await Task.Delay(TimeSpan.FromSeconds(4));
onEmailClicked(id: 10);
await Task.Delay(TimeSpan.FromSeconds(2));
onEmailClicked(id: 20);
await Task.Delay(TimeSpan.FromSeconds(1));
onEmailClicked(id: 30);
await Task.Delay(TimeSpan.FromSeconds(4));
#endregion T E S T
编辑
虽然我意识到该帖子不再有除任务取消之外的娱乐性“其他”选项的措辞,但我仍然想通过将WatchDogTimer
放入 class 来合并优秀评论并改进我的原始答案......
class WatchDogTimer
{
int _wdtCount = 0;
public TimeSpan Interval { get; set; } = TimeSpan.FromSeconds(1);
public void ThrowBone(Action action)
{
_wdtCount++;
var capturedCount = _wdtCount;
Task
.Delay(Interval)
.GetAwaiter()
.OnCompleted(() =>
{
// If the 'captured' localCount has not changed after
// awaiting the Interval, it indicates that no new
// 'bones' have been thrown during that interval.
if (capturedCount.Equals(_wdtCount))
{
action();
}
});
}
}
...并从 pos 中消除async void
。
void onEmailClicked(int id)
=> _watchDog.ThrowBone(() => MarkEmailAsRead(id));
void MarkEmailAsRead(int id)
{
// BeginInvoke(()=>
// {
Console.WriteLine($"Expired: {_stopWatch.Elapsed}");
Console.WriteLine($"The email with the ID '{id}' has been marked read");
// });
}
试验台
#region T E S T
// Same test parameters as before.
System.Diagnostics.Stopwatch _stopWatch =
new System.Diagnostics.Stopwatch();
WatchDogTimer _watchDog =
new WatchDogTimer { Interval = TimeSpan.FromSeconds(3) };
_stopWatch.Start();
Console.WriteLine(_stopWatch.Elapsed);
onEmailClicked(id: 1);
await Task.Delay(TimeSpan.FromSeconds(1));
Console.WriteLine(_stopWatch.Elapsed);
onEmailClicked(id: 2);
await Task.Delay(TimeSpan.FromSeconds(1));
Console.WriteLine(_stopWatch.Elapsed);
onEmailClicked(id: 3);
await Task.Delay(TimeSpan.FromSeconds(4));
Console.WriteLine(_stopWatch.Elapsed);
onEmailClicked(id: 10);
await Task.Delay(TimeSpan.FromSeconds(2));
Console.WriteLine(_stopWatch.Elapsed);
onEmailClicked(id: 20);
await Task.Delay(TimeSpan.FromSeconds(1));
Console.WriteLine(_stopWatch.Elapsed);
onEmailClicked(id: 30);
await Task.Delay(TimeSpan.FromSeconds(4));
#endregion T E S T
首先,我假设您的代码是单线程的。 CancellationTokenSource.Dispose
方法不是线程安全的,因此尝试处置在多线程环境中共享的CancellationTokenSource
是一场小小的噩梦。 您可以通过在与CancellationTokenSource
交互的代码中搜索Task.Run
和 .ConfigureAwait .ConfigureAwait(false)
来检查您的代码是否是单线程的。 如果你找到任何东西,你可能会遇到麻烦。
其次,您应该知道CancellationTokenSource.Cancel
方法是“不稳定的”。 它立即调用之前通过CancellationToken.Register
路由注册的所有回调。 像await Task.Delay(3000, _cts.Token);
这样的代码隐式注册回调,因此执行流程可能会跳转到await
之后的代码,然后继续执行Cancel
之后的代码。 您的代码应该为此类跳转做好准备。 您不应该假设Cancel
会很快完成,也不应该假设它不会抛出。 如果注册的回调失败,则Cancel
调用将显示异常。
至于你的代码的细节,你的错误是你与_cts
领域的互动太多了。 您应该将交互减少到最低限度。 您还应该假设在await
之后_cts
不会存储您在await
之前分配的相同实例。 您的代码是单线程的,但是是异步的。 异步代码具有复杂的执行流程。 我同意 Gabriel Luci 的观点,你应该只在一个地方Dispose
。 使用async void
并没有那么糟糕,因为 UI 事件处理程序通常为async void
。 当您完成应用程序并尝试关闭 Window 时,它可能会咬住您,因为您将无法在关闭之前等待挂起的异步操作完成。 所以你可能会在关闭 Window 后遇到烦人的错误,因为异步操作会尝试与已被释放的 UI 组件进行交互。 理想情况下,您应该存储最新异步操作( Task
)的引用,并在开始下一个操作之前或在关闭 Window 之前等待它。但是为了保持答案简单,我将省略这种复杂性。 这是我的建议:
private CancellationTokenSource _cts;
private async void OnEmailClicked(int id)
{
_cts?.Cancel();
CancellationTokenSource cts = new();
try
{
_cts = cts;
await LoadEmail(id);
try { await Task.Delay(3000, cts.Token); }
catch (OperationCanceledException) { return; } // Do nothing
}
finally
{
if (cts == _cts) _cts = null;
cts.Dispose();
}
await MarkEmailAsRead(id);
}
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.