В этом посте я попытаюсь показать проблему, решение и истоки.
Проблема
Пусть у нас есть код, который выглядит так:
static async Task<dynamic> Compute(Task<dynamic> inner)
{
return await Task.Factory.StartNew(async () => await inner);
}
Вопрос
Думаете, речь идет о пропущенном ConfigureAwait? Хаха!
NB: вопрос о ConfigureAwait я опущу, ибо о другом статья.
Истоки
До идиомы async/await основным способом использования Tasks API был метод Task.Factory.StartNew() с кучей перегрузок. Так, Task.Run() немного облегчает данный подход, опуская указание планировщика (TaskScheduler) и т.п.
static Task<T> Run<T>(Func<T> inner)
{
return Task.Run(inner);
}
static Task<T> RunFactory<T>(Func<T> inner)
{
return Task.Factory.StartNew(inner);
}
Ничего особенно в примере выше нет, но именно здесь начинаются отличия, и возникает главная проблема — многие начинают думать, что Task.Run() — это облегченный Task.Factory.StartNew().
Однако это не так!
Чтобы стало нагляднее, рассмотрим пример:
static Task<T> Compute<T>(Task<T> inner)
{
return Task.Run(async () => await inner);
}
static async Task<T> ComputeWithFactory<T>(Task<T> inner)
{
return await await Task.Factory.StartNew(async () => await inner);
}
Что? Два await'a? Именно так.
Все дело в перегрузках:
public static Task<TResult> Run<TResult>(Func<Task<TResult>> function)
{
// code
}
public Task<TResult> StartNew<TResult>(Func<TResult> function)
{
// code
}
Несмотря на то, что возвращаемый тип у обоих методов — Task<TResult>, входным параметром у Run является Func<Task<TResult>>.
В случае с
async () => await inner
Task.Run получит уже готовую state-машину (а мы знаем, что await — есть не что иное, как трансформация кода в state-машину), где все оборачивается в Task.StartNew получит то же самое, но TResult уже будет Task<Task<T>>.
— OK, но почему изначальный пример не падает с ошибкой компиляции, т.к. отсутствует второй await?
Ответ: dynamic.
В одной статье, я уже описывал работу dynamic: каждый statement в C# превращается в узел вызова (call-site), который относится ко времени исполнения, а не компиляции. При этом сам компилятор старается побольше метаданных передать рантайму.
Метод Compute() использует и возвращает Task<dynamic>, что заставляет компилятор создавать эти самые узлы вызовов.
Причем, это корректный код — результатом в рантайме будет Task<Task<dynamic>>.
Решение
Оно весьма простое: необходимо использовать метод Unwrap().
В коде без dynamic вместо двух await'ов можно обойтись одним:
static async Task<T> ComputeWithFactory<T>(Task<T> inner)
{
return await Task.Factory.StartNew(async () => await inner).Unwrap();
}
И применить к
static async Task<dynamic> Compute(Task<dynamic> inner)
{
return await Task.Factory.StartNew(async () => await inner).Unwrap();
}
Теперь, как и ожидалось, результатом будет Task<dynamic>, где dynamic — именно возвращаемое значение inner'a, но не еще один таск.
Выводы
Всегда используйте метод-расширение Unwrap для Task.Factory.StartNew(). Это сделает ваш код более идиоматичным (один await на вызов) и не допустит хитростей dynamic.
Task.Run() — для обычных вычислений.
Task.Factory.StartNew() + Unwrap() — для обычных вычислений с указанием TaskScheduler'a и т.д.
Комментарии (7)
g_DiGGeR
28.03.2016 18:26А зачем вообще сложности типа использования async/await когда результатом функции является задача?
Зачем передавать в создание таски async () => await inner.
Это же и есть причина ошибки.
Bronx
30.03.2016 08:42> есть ли в данном примере проблема?
Да, есть проблема, но, как и у g_DiGGeR, проблема в понимании: зачем в Compute передавать Task; a если уж приспичило передавать именно Task, то зачем его там стартовать вместо того, чтобы сразу отдать (безо всякого return await); а если вдруг нужно дождаться результата inner и использовать внутри Compute, то почему не использовать await inner напрямую, без StartNew с асинхронной блямбдой и т.п.?
> Всегда используйте метод-расширение Unwrap для Task.Factory.StartNew()
Вызывать Unwrap после StartNew нужно лишь в случае когда передаётся асинхронный делегат в качестве параметра (как в вашем примере), потому что такой делегат вернёт Task, который завернётся в ещё один Task, и его нужно развернуть. В прочих случаях никакой Unwrap не нужен.
См. http://blogs.msdn.com/b/pfxteam/archive/2011/10/24/10229468.aspx
А dynamic тут вообще не при чём — Unwrap нужен с любым типом результата. Task.Run уже содержит внутри Unwrap() (там, где это нужно), так что да, Run — это не просто StartNew с дефолтными параметрами.
Вместо Unwrap можно использовать двойной await (return await await ...)szKarlen
30.03.2016 12:27Вызывать Unwrap после StartNew нужно лишь в случае когда передаётся асинхронный делегат в качестве параметра (как в вашем примере), потому что такой делегат вернёт Task, который завернётся в ещё один Task, и его нужно развернуть. В прочих случаях никакой Unwrap не нужен.
Естественно. об этом и речь. Так в статье я писал, что все дело в перегрузках:
public static Task<TResult> Run<TResult>(Func<Task<TResult>> function) public Task<TResult> StartNew<TResult>(Func<TResult> function);
Несмотря на то, что возвращаемый тип у обоих методов — Task, входным параметром у Run является Func<Task<TResult>>.
Кстати у Вас идет противоречие:
В прочих случаях никакой Unwrap не нужен
+
Unwrap нужен с любым типом результата
+
Вместо Unwrap можно использовать двойной await (return await await ...)
т.е. Вы статью не читали, так? ибо особенно показательно я призываю не использовать просто так два await'a подряд:
Всегда используйте метод-расширение Unwrap для Task.Factory.StartNew(). Это сделает ваш код более идиоматичным (один await на вызов) и не допустит хитростей dynamic.
Чтобы стало нагляднее, рассмотрим пример:
static Task<T> Compute<T>(Task<T> inner) { return Task.Run(async () => await inner); }
static async Task<T> ComputeWithFactory<T>(Task<T> inner) { return await await Task.Factory.StartNew(async () => await inner); }
И, естественно, если читатель понял контекст всей статьи, что мы рассматриваем именно не-void случаи, тогда таких вопросов не должно возникать.
Кстати, если будет выбрана перегрузка с простым Action, а внутри окажется таск, то, соответсвенно, это уже будет асинхронная лямбда с возвращаемым типом void.
Кажется, в каждом туториале пишут большими буквами: никогда не используйте async void.
Мне это пересказывать? зачем? Если читатель не знаком с данным постулатом, то и остальное не пригодится.
И последнее:
Да, есть проблема, но, как и у g_DiGGeR, проблема в понимании: зачем в Compute передавать Task; a если уж приспичило передавать именно Task, то зачем его там стартовать вместо того, чтобы сразу отдать (безо всякого return await); а если вдруг нужно дождаться результата inner и использовать внутри Compute, то почему не использовать await inner напрямую, без StartNew с асинхронной блямбдой и т.п.?
In real life так просто не будет. Рефакторинги происходят, код пишется разными людьми. Мира с единорогами нет, где, допустим, в ComputeAsync попадет что-простое. А уж таких ComputeAsync с проблемами было n-ое кол-во.
Моя мысль простая: один await на async метод. Если возвращаемый тип Task<Task<T>>, тогда Unwrap() нужен, ибо для Task<T> бесполезно.
Bronx
30.03.2016 13:07> Кстати у Вас идет противоречие:
> В прочих случаях никакой Unwrap не нужен
> +
> Unwrap нужен с любым типом результата
Под «любым типом результата» подразумевался не результат функции, а TResult — хоть dynamic, хоть int. Так что тут нет противоречия.
С двойным await каюсь, был невнимателен.
> если будет выбрана перегрузка с простым Action, а внутри окажется таск, то, соответсвенно, это уже будет асинхронная лямбда с возвращаемым типом void.
Не совсем понял, внутри чего окажется таск? И я что-то в три часа ночи уже не соображу, как совместить перегрузку по Action (который и сам синхронный и не возвращает ничего асинхронного) с асинхронной лямбдой? Ну если только не ставить async-и и лямбды бездумно куда ни попадя.szKarlen
30.03.2016 13:38Не совсем понял, внутри чего окажется таск? И я что-то в три часа ночи уже не соображу, как совместить перегрузку по Action (который и сам синхронный и не возвращает ничего асинхронного) с асинхронной лямбдой? Ну если только не ставить async-и и лямбды бездумно куда ни попадя.
Я имел ввиду момент, когда даже если передается Action, то последний может быть помечен async модификатором как из-за await'a внутри тела определенного таска, так и по ошибке.
Например:
static void ComputeNoReturn(Action inner) { inner(); } ComputeNoReturn(async () => { throw new Exception(); }); ComputeNoReturn(async () => { await Task.Run(() => {}); });
Так что, просто Action и async Action — две разные вещи, т.к. обрабатываются компилятором совершенно по-разному. Ну и в тему.
Melz
Меня терзают сомнения на счет полезности dynamic.
Для вдохновления могу посоветовать почитать:
Processing Sequences of Asynchronous Operations with Task
http://blogs.msdn.com/b/pfxteam/archive/2010/11/21/10094564.aspx?Redirected=true
The task monad in C#
http://ruudvanasseldonk.com/2013/05/01/the-task-monad-in-csharp
Razaz
Dapper?