Ответ 1
Это отличный улов - и я согласен с тем, что в CTP есть ошибка. Я впился в него и вот что происходит:
Это комбинация реализации CTP преобразований асинхронного компилятора, а также существующего поведения TPL (параллельной библиотеки задач) из .NET 4.0+. Вот факторы, которые играют:
- Наконец, тело из источника переведено в часть реального тела CLR-finally. Это желательно по многим причинам, одним из которых является то, что мы можем заставить CLR выполнить его, не перехватывая исключение дополнительное время. Это также упрощает наш код gen до некоторой степени - более простой код gen приводит к меньшим двоичным файлам после компиляции, что определенно желательно для многих наших клиентов.:)
- Общий
Task
для методаFunc(int n)
является реальной задачей TPL. Когда выawait
вConsumer()
, тогда остальная часть методаConsumer()
фактически устанавливается как продолжение завершения завершенияTask
, возвращаемого изFunc(int n)
. - То, как компилятор CTP преобразует методы async, приводит к тому, что
return
сопоставляется с вызовомSetResult(...)
до реального возврата.SetResult(...)
сводится к вызовуTaskCompletionSource<>.TrySetResult
. -
TaskCompletionSource<>.TrySetResult
сигнализирует о завершении задачи TPL. Мгновенное включение его продолжений "когда-то". Это "когда-то" может означать на другом потоке, или в некоторых условиях TPL умный и говорит "um, я мог бы просто просто позвонить ему сейчас в этом же потоке". - Обобщающий
Task
дляFunc(int n)
становится технически "завершенным" до того, как окончательно запущен. Это означает, что код, ожидающий метода async, может выполняться в параллельных потоках или даже до блока finally.
Учитывая, что всеобъемлющий Task
должен представлять асинхронное состояние метода, в принципе он не должен быть помечен как завершенный, пока, по крайней мере, весь предоставленный пользователем код не будет выполнен в соответствии с языком. Я расскажу об этом вместе с Андерсом, командой разработчиков языка и разработчиками компилятора, чтобы это выглядело.
Область проявления/серьезность:
Как правило, в случае WPF или WinForms, где у вас есть какой-то управляемый цикл сообщений, вы, как правило, не ошибаетесь. Причина в том, что реализации await
on Task
откладываются до SynchronizationContext
. Это приводит к тому, что продолжения асинхронизации будут помещены в очередь в уже существующем цикле сообщений, который будет запущен в том же потоке. Вы можете проверить это, изменив код для запуска Consumer()
следующим образом:
DispatcherFrame frame = new DispatcherFrame(exitWhenRequested: true);
Action asyncAction = async () => {
await Consumer();
frame.Continue = false;
};
Dispatcher.CurrentDispatcher.BeginInvoke(asyncAction);
Dispatcher.PushFrame(frame);
После запуска внутри контекста цикла сообщений WPF вывод будет выглядеть так, как вы ожидали:
Consumer: before await #1
Func: Begin #1
Func: End #1
Func: Finally #1
Consumer: after await #1
Consumer: before await #2
Func: Begin #2
Func: End #2
Func: Finally #2
Consumer: after await #2
Consumer: after the loop
After the wait
Обход проблемы:
Увы, обходной путь означает изменение вашего кода, чтобы не использовать выражения return
внутри блока try/finally
. Я знаю, это действительно означает, что вы теряете много элегантности в своем потоке кода. Вы можете использовать асинхронные вспомогательные методы или вспомогательные лямбда, чтобы обойти это. Лично я предпочитаю helper-lambdas, потому что он автоматически закрывает локаль/параметры из содержащего метода, а также сохраняет ваш соответствующий код ближе.
Помощник Лямбда:
static async Task<int> Func( int n )
{
int result;
try
{
Func<Task<int>> helperLambda = async() => {
Console.WriteLine( " Func: Begin #{0}", n );
await TaskEx.Delay( 100 );
Console.WriteLine( " Func: End #{0}", n );
return 0;
};
result = await helperLambda();
}
finally
{
Console.WriteLine( " Func: Finally #{0}", n );
}
// since Func(...) return statement is outside the try/finally,
// the finally body is certain to execute first, even in face of this bug.
return result;
}
Метод метода помощника:
static async Task<int> Func(int n)
{
int result;
try
{
result = await HelperMethod(n);
}
finally
{
Console.WriteLine(" Func: Finally #{0}", n);
}
// since Func(...) return statement is outside the try/finally,
// the finally body is certain to execute first, even in face of this bug.
return result;
}
static async Task<int> HelperMethod(int n)
{
Console.WriteLine(" Func: Begin #{0}", n);
await TaskEx.Delay(100);
Console.WriteLine(" Func: End #{0}", n);
return 0;
}
Как бесстыдный плагин: мы нанимаем на пространстве языков в Microsoft и всегда ищем большой талант. Запись в блоге здесь с полным списком открытых позиций:)