суббота, 21 декабря 2013 г.

async/await и cancellation

Всем привет! Прошу прощения за то что долго не писал. Вынашивал планы родить две больших статьи, но то ли не выносил, то ли не родил. А тут подвернулась тема для маленькой заметочки. Будет приятно, если кому-то пригодится. Итак,

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