Превращаем синхронную операцию в асинхронную

Рассмотрим следующую задачу: необходимо вызывать некий синхронный код как асинхронный.

На первый взгляд, сделать это в .NET 4.5 просто, используя класс TaskCompletionSource:

internal class SyncToAsyncDemo
{
    public int SomeAction()
    {
        Thread.Sleep(500);
        return 5;
    }

    public Task<int> SomeActionAsync()
    {
        var tcs = new TaskCompletionSource<int>();
        Task.Run(() => {
            var result = this.SomeAction();
            tcs.SetResult(result);
        });

        return tcs.Task;
    }
}

// Где-то в клиентском коде

var demo = new SyncToAsyncDemo();
int result = await demo.SomeActionAsync();
Console.WriteLine("Result = {0}", result);

Если теперь приведенный код скопировать в приложение и запустить, то он будет работать без проблем.

Небольшое отступление: чем TaskCompletionSource лучше простого return Task.Run(…)? В простых случаях (как выше) это не так очевидно, но в коде нет связи возвращаемого Task и какого-либо делегата. Что это дает? Пусть, например, метод возвращает данные из СУБД и есть кэш. Тогда, если результат уже был загружен, его можно вернуть сразу, вместо вызова Task.Run(). Это позволит не занимать поток, пусть даже не надолго, и не нести соответствующие накладные расходы.

Другой вариант использования TaskCompletionSource можно найти в более сложных сценариях, когда tcs.SetResult() вызывается в другом блоке кода. Так, например, “переделываются” в новый стиль старые асинхронные методы, которые вызывают делегат по завершению операции.

Но вернемся к рассматриваемому коду. Его проблема в том, что в реальном приложении, где вместо Thread.Sleep() делается что-то полезное, может появиться блокировка выполнения и вызов SomeActionAsync() никогда не завершится.

Причину легко понять, если посмотреть что будет, если SomeAction() выбросит любое исключение. А в реальной задаче это может сделать не только он сам, но и любой из используемых им методов (и так далее по цепочке вызовов). Теперь код никогда не дойдет до tcs.SetResult(), а значит созданный Task не перейдет в завершенное состояние.

Для решения этой проблемы, класс TaskCompletionSource предоставляет метод SetException(), позволяющий передать исключение вызывающему коду. Перепишем SomeActionAsync():

public Task<int> SomeActionAsync()
{
    var tcs = new TaskCompletionSource<int>();
    Task.Run(() => {
        try {
            var result = this.SomeAction();
            tcs.SetResult(result);
        }
        catch (Exception ex) {
            tcs.SetException(ex);
        }
    });

    return tcs.Task;
}

Теперь вызывающий код, с помощью обычного try-catch, сможет поймать и обработать любые возникшие внутри синхронного метода исключения. И, что очень важно, не будет блокировки выполнения кода.