С TPL у нас есть CancellationTokenSource
, который предоставляет токены, полезные для совместной отмены текущего задания (или его запуска).
Вопрос:
Как долго этопринять, чтобы распространить запрос отмены на все подключенные запущенные задачи?Есть ли место, где код мог бы посмотреть, чтобы проверить, что «отныне» каждый заинтересованный Task
найдет, что запрос на отмену был запрошен?
Зачем это нужно?
Я хотел бы иметь стабильный модульный тест, чтобы показать, что в нашем коде работает отмена.
Детали проблемы:
У нас есть "Executor", который создает задачи, эти задачи переносятся довольно долгобегущие действия.Основная задача исполнителя - ограничить количество одновременных действий.Все эти задачи могут быть отменены по отдельности, а также эти действия будут учитывать CancellationToken
внутренне.
Я хотел бы предоставить модульный тест, который показывает, что когда отмена произошла, в то время как задача ожидает slot для запуска заданного действия , эта задача отменит себя (в конце концов) и не начнет выполнение заданного действия .
Итак, идея заключалась в подготовке LimitingExecutor
с одним слотом .Затем запустите блокирующее действие , которое будет запрашивать отмену при разблокировании.Затем «поставьте в очередь» тестовое действие , которое должно завершиться неудачно при выполнении.При такой настройке тесты будут вызывать разблокировать , а затем утверждать, что задача тестовое действие выдаст 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
), на моем компьютере может быть 1 из 10 запусков.Вид "решения" этой проблемы - вставить Thread.Sleep
сразу после запроса на отмену.Даже при 3-секундном сне этот тест иногда дает сбой (обнаруживается после 20-ти прогонов), и когда он проходит, такое долгое ожидание обычно не требуется (я полагаю).Для справки см. diff .
"Другая проблема", чтобы гарантировать, что отмена происходит из "времени ожидания", а не из Task.Run
, потому что ThreadPool
может быть занят (другие выполняющие тесты), и это откладывает запуск второго задания после запроса отмены - что сделало бы этот тест «ложно-зеленым».«Легкое исправление взломом» состояло в том, чтобы активно ждать, пока не запустится вторая задача - ее Status
становится TaskStatus.Running
.Пожалуйста, проверьте версию под этой веткой и убедитесь, что тест без этого взлома будет иногда "зеленым" - поэтому пробная ошибка может пройти через него.