Это отличный улов - и я согласен, что на самом деле здесь есть ошибка в CTP. Я копался в этом, и вот что происходит:
Это комбинация CTP-реализации преобразований асинхронного компилятора, а также существующего поведения TPL (Task Parallel Library) из .NET 4.0+. Вот факторы в игре:
- Тело finally из источника переводится в часть реального тела CLR-finally. Это желательно по многим причинам, одна из которых заключается в том, что мы можем заставить CLR выполнить его, не перехватывая / не перебрасывая исключение в дополнительное время. Это также в некоторой степени упрощает нашу генерацию кода - более простая генерация кода приводит к меньшим двоичным файлам после компиляции, что определенно желательно многими нашими клиентами. :)
- Общая
Task
для метода Func(int n)
- настоящая задача TPL. Когда вы await
в Consumer()
, тогда остальная часть метода Consumer()
фактически устанавливается как продолжение завершения Task
, возвращенного из Func(int n)
.
- То, как компилятор CTP преобразует асинхронные методы, приводит к отображению
return
в вызов SetResult(...)
до реального возврата. SetResult(...)
сводится к звонку на TaskCompletionSource<>.TrySetResult
.
TaskCompletionSource<>.TrySetResult
сигнализирует о завершении задачи TPL. Мгновенно позволяя его продолжениям происходить «когда-нибудь». Это «когда-нибудь» может означать в другом потоке, или в некоторых условиях TPL умный и говорит: «Ну, я мог бы просто вызвать его сейчас в этом же потоке».
- Всеохватывающий
Task
для Func(int n)
становится технически "Завершенным" прямо перед тем, как, наконец, запускается. Это означает, что код, ожидающий асинхронного метода , может выполняться в параллельных потоках или даже перед блоком finally.
Учитывая, что в целом Task
должен представлять асинхронное состояние метода, принципиально он не должен быть помечен как завершенный, пока, по крайней мере, весь предоставленный пользователем код не будет выполнен согласно языковой схеме. Я расскажу об этом Андерсу, команде разработчиков языков и разработчикам компиляторов, чтобы на это взглянуть.
Объем проявления / серьезность:
Как правило, вас это не расстроит в случае WPF или WinForms, когда у вас есть какой-то управляемый цикл обработки сообщений. Причина в том, что реализации await
на 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, потому что он автоматически закрывает локальные параметры / параметры из содержащего метода, а также сохраняет ваш соответствующий код ближе.
Подход Helper Lambda:
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(...)'s 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(...)'s 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 и всегда ищем талантливых людей. Запись в блоге здесь с полным списком открытых позиций:)