[英]Propagation time of cancellation request to all tasks (TPL)
使用TPL,我們有CancellationTokenSource
,它提供令牌,可用於以協作方式取消當前任務(或其開始)。
將取消請求傳播到所有已掛起的正在運行的任務需要多長時間? 有什么地方的代碼可以用來檢查以下內容:從現在開始,每一個感興趣的Task
都會發現已請求取消嗎?
我想進行穩定的單元測試,以表明取消在我們的代碼中有效。
我們有產生任務的“執行程序”,這些任務包裝了一些長時間運行的動作。 執行程序的主要工作是限制啟動了多少個並發動作。 所有這些任務都可以單獨取消,並且這些操作也將在內部遵守CancellationToken
。
我想提供單元測試,該測試表明當任務等待插槽開始給定操作時發生取消操作時 ,該任務將自行取消(最終),並且不會開始執行給定操作 。
因此,想法是用單個插槽准備LimitingExecutor
。 然后開始阻止操作 ,該操作將在取消阻止時請求取消。 然后“排隊” 測試操作 ,該操作應在執行時失敗。 使用該設置,測試將調用unblock ,然后斷言測試操作的任務將在等待時拋出TaskCanceledException
。
[Test]
public void RequestPropagationTest()
{
using (var setupEvent = new ManualResetEvent(initialState: false))
using (var cancellation = new CancellationTokenSource())
using (var executor = new LimitingExecutor())
{
// System-state setup action:
var cancellingTask = executor.Do(() =>
{
setupEvent.WaitOne();
cancellation.Cancel();
}, CancellationToken.None);
// Main work action:
var actionTask = executor.Do(() =>
{
throw new InvalidOperationException(
"This action should be cancelled!");
}, cancellation.Token);
// Let's wait until this `Task` starts, so it will got opportunity
// to cancel itself, and expected later exception will not come
// from just starting that action by `Task.Run` with token:
while (actionTask.Status < TaskStatus.Running)
Thread.Sleep(millisecondsTimeout: 1);
// Let's unblock slot in Executor for the 'main work action'
// by finalizing the 'system-state setup action' which will
// finally request "global" cancellation:
setupEvent.Set();
Assert.DoesNotThrowAsync(
async () => await cancellingTask);
Assert.ThrowsAsync<TaskCanceledException>(
async () => await actionTask);
}
}
public class LimitingExecutor : IDisposable
{
private const int UpperLimit = 1;
private readonly Semaphore _semaphore
= new Semaphore(UpperLimit, UpperLimit);
public Task Do(Action work, CancellationToken token)
=> Task.Run(() =>
{
_semaphore.WaitOne();
try
{
token.ThrowIfCancellationRequested();
work();
}
finally
{
_semaphore.Release();
}
}, token);
public void Dispose()
=> _semaphore.Dispose();
}
這個問題的可執行演示(通過NUnit)可以在GitHub上找到 。
但是,該測試的實現有時會失敗(沒有預期的TaskCanceledException
),在我的機器上可能有十分之一的運行。 解決該問題的一種方法是在取消請求之后立即插入Thread.Sleep
。 即使睡眠3秒鍾,該測試有時仍會失敗(運行20次后才能發現),並且通過測試后,通常無需長時間等待(我想)。 作為參考,請參見diff 。
“其他問題”,是為了確保取消來自“等待時間”,而不是來自Task.Run
,因為ThreadPool
可能很忙(其他正在執行測試),並且它在取消請求后將第二個任務的啟動推遲-這將導致此測試為“偽綠色”。 “通過黑客輕松解決”是主動等待直到第二個任務開始-其Status
變為TaskStatus.Running
。 請檢查此分支下的版本,看看沒有此hack的測試有時會是“綠色”-因此示例錯誤可能會通過。
您的測試方法假定cancellingTask
始終在actionTask
之前actionTask
LimitingExecutor
的插槽(輸入信號量)。 不幸的是,這個假設是錯誤的, LimitingExecutor
不能保證這一點,這只是運氣,這兩個任務中的哪一個占用了插槽(實際上在我的計算機上,它僅發生在5%的運行次數中)。
要解決此問題,您需要另一個ManualResetEvent
,它將允許主線程等待,直到cancellingTask
實際占用插槽為止:
using (var slotTaken = new ManualResetEvent(initialState: false))
using (var setupEvent = new ManualResetEvent(initialState: false))
using (var cancellation = new CancellationTokenSource())
using (var executor = new LimitingExecutor())
{
// System-state setup action:
var cancellingTask = executor.Do(() =>
{
// This is called from inside the semaphore, so it's
// certain that this task occupies the only available slot.
slotTaken.Set();
setupEvent.WaitOne();
cancellation.Cancel();
}, CancellationToken.None);
// Wait until cancellingTask takes the slot
slotTaken.WaitOne();
// Now it's guaranteed that cancellingTask takes the slot, not the actionTask
// ...
}
.NET Framework不提供用於檢測任務向Running
狀態轉換的API,因此,如果您不喜歡在循環中輪詢State
屬性+ Thread.Sleep()
,則需要將LimitingExecutor.Do()
修改為提供此信息,可能使用另一個ManualResetEvent
,例如:
public Task Do(Action work, CancellationToken token, ManualResetEvent taskRunEvent = null)
=> Task.Run(() =>
{
// Optional notification to the caller that task is now running
taskRunEvent?.Set();
// ...
}, token);
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.