Andrey on .NET | Превращаем синхронную операцию в асинхронную

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

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

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

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

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

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

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

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

Комментарии (6) -

Можно принципиальный вопрос?
Чем:
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;
}


лучше простого

public Task<int> SomeActionAsync()
{
    return Task.Run(() => SomeAction());
}


@ Lion: Дополнил статью.

@ Andrey: Меня не убедил ответ Frown
Статья называется Превращаем синхронную операцию в асинхронную
и самое простое решение, это как сказал Lion
TaskCompletionSource<T> можно использовать для кеширования, но в статье/коде не используется => лишнее.

Всё проще. Асинхронка - понятие общее, и работа через пул потоков - частный случай организации асинхронки. Не всегда для асинхронной задачи может быть нужен поток. Бывают асинхронные задачи, где потоки вообще не нужны. Пример с базой - уж совсем мудрёный, и новичков только запутает. Простой пример - можно использовать класс Process для запуска, например, notepad, и подписаться на эвент его закрытия. Согласитесь, что тут никакой тред не нужен, а операция ожидания закрытия блокнота всё-равно является асинхронной. Чтоб не изобретать велосипед, придумали TaskCompletionSource который в примере создаст таск ожидания закрытия не выполняя вообще ничего, кроме установки результата, как только закроют блокнот.

Подскажите пожаулйста почему не правильней было бы написать:

try {
   Task.Run(() => {
            var result = this.SomeAction();
            tcs.SetResult(result);
    });
}
catch (Exception ex) {
   tcs.SetException(ex);
}

Ведь если вместо Task.Run, будет SomeFunctionWithDelegate(() => {}), то ошибка может произойти внутри функции, не лучше ли тогда ее тоже пробросить ожидающему потоку?

Task.Run создает задачу, которая будет выполняться в отдельном потоке. Выполнение кода продолжится и выйдет за пределы блока try...catch практически сразу после запуска задачи.

Исключение из SomeAction будет выброшено в отдельном потоке. Как результат, ваш catch его не поймает, потому что это (1) другой поток, (2) выполнение кода уже ушло дальше catch.

Рассматривайте код внутри Run как отдельный поток. А из метода вы возвращаем Task с помощью которого мы можем контролировать, завершилась ли задача и получить результат работы (значение или exception).

Добавить комментарий