столкнулся недавно с необходимостью отмены асинхронных вычислений, выполненных на async программной модели .NET Framework 4.5. Воспользовался официальным блогом как руководством к действию и получил примерно следующий код:
try
{
var bytesRead = await stream.ReadAsync(
buffer, 0, bytesToRead, cancellationToken);
// some code here
}
catch(TaskCancelledException)
{
break;
}
Меня смутил подход, при котором индикатором отмены задачи является исключение. Во-первых, у меня предубеждение по поводу использования исключений не по назначению (а здесь оно явно лишнее). Во-вторых, Debug Output окно подзамусоривается текстом о возникших исключениях.Примечательно то, что при использовании методов Task.ContinueWith исключение TaskCancelledException не возбуждается. Вместо этого мы имеем экземпляр Task, у которого после его отмены свойство IsCanceled установлено в true. Исключение будет возбуждено либо при вызове метода Task.Wait(), либо при обращении к свойству Task.Result.
Все верно, async/await паттерн подразумевает что после старта задачи код получает управление лишь при завершении или отмены задачи. При этом await конструкция вытягивает из задачи результат. Именно это вытягивание и приводит к возбуждению исключения. Обидно, но отказываться от async/await модели не хочется.
Вот тут и пришла идея поднять Task на уровень выше, т.е. await-ить не Task<int>, а Task<Task<int>>, обернув нужный Task в еще один Task с помощью примерно такого кода:
public static Task<Task<T>> Wrap<T>(this Task<T> task)
{
var tsource = new TaskCompletionSource<Task<T>>();
task.ContinueWith(tsource.SetResult);
return tsource.Task;
}
public static Task<Task> Wrap(this Task task)
{
var tsource = new TaskCompletionSource<Task>();
task.ContinueWith(tsource.SetResult);
return tsource.Task;
}
Можем воспользоваться этими методами следующим образом:var tBytesRead = await stream
.ReadAsync(buffer, 0, bytesToRead, cancellationToken)
.Wrap();
if (tBytesRead.IsCanceled)
break;
var bytesRead = tBytesRead.Result;
Теперь await ловит не только успешно завершенные задачи, но и отмененные. Осталось лишь пощупать IsCanceled и обратиться к свойству Result за результатом операции в случае если она не была отменена.P.S. Приятный бонус заключается в том, что при моделировании мгновенной отмены операции Task.Delay(100, cancellationToken) подход с оборачиванием задачи показал примерно 4-х кратное преимущество по времени выполнения относительно подхода с поимкой возбужденного исключения.