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