[英]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.