столкнулся недавно с необходимостью отмены асинхронных вычислений, выполненных на 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-х кратное преимущество по времени выполнения относительно подхода с поимкой возбужденного исключения.