Подтвердить что ты не робот

Async CTP и "наконец"

Здесь код:

static class AsyncFinally
{
    static async Task<int> Func( int n )
    {
        try
        {
            Console.WriteLine( "    Func: Begin #{0}", n );
            await TaskEx.Delay( 100 );
            Console.WriteLine( "    Func: End #{0}", n );
            return 0;
        }
        finally
        {
            Console.WriteLine( "    Func: Finally #{0}", n );
        }
    }

    static async Task Consumer()
    {
        for ( int i = 1; i <= 2; i++ )
        {
            Console.WriteLine( "Consumer: before await #{0}", i );
            int u = await Func( i );
            Console.WriteLine( "Consumer: after await #{0}", i );
        }
        Console.WriteLine( "Consumer: after the loop" );
    }

    public static void AsyncTest()
    {
        Task t = TaskEx.RunEx( Consumer );
        t.Wait();
        Console.WriteLine( "After the wait" );
    }
}

Здесь вывод:

Consumer: before await #1
    Func: Begin #1
    Func: End #1
Consumer: after await #1
Consumer: before await #2
    Func: Begin #2
    Func: Finally #1
    Func: End #2
Consumer: after await #2
Consumer: after the loop
    Func: Finally #2
After the wait

Как вы можете видеть, блок finally выполняется намного позже, чем вы ожидали.

Любые обходные пути?

Спасибо заранее!

4b9b3361

Ответ 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 и всегда ищем большой талант. Запись в блоге здесь с полным списком открытых позиций:)

Ответ 2

Изменить

Пожалуйста, рассмотрите Theo Yaung ответ.

Оригинальный ответ

Я не знаком с async/wait, но после прочтения этого: Обзор Visual Studio Async CTP

и прочитав ваш код, я вижу await в функции Func(int n), что означает, что из кода после ключевое слово await до конца функции будет исполнено позже как делегат.

Итак, мое предположение (и это необразованное предположение) состоит в том, что Func:Begin и Func:End могут выполняться в разных "контекстах" (потоках?), то есть асинхронно.

Таким образом, строка int u = await Func( i ); в Consumer продолжит свое выполнение момента, когда будет достигнут код await в Func. Поэтому вполне возможно:

Consumer: before await #1
    Func: Begin #1
Consumer: after await #1
Consumer: before await #2
    Func: Begin #2
Consumer: after await #2
Consumer: after the loop
    Func: End #1         // Can appear at any moment AFTER "after await #1"
                         //    but before "After the wait"
    Func: Finally #1     // will be AFTER "End #1" but before "After the wait"
    Func: End #2         // Can appear at any moment AFTER "after await #2"
                         //    but before "After the wait"
    Func: Finally #2     // will be AFTER "End #2" but before "After the wait"
After the wait           // will appear AFTER the end of all the Tasks

Func: End и Func: Finally могут отображаться в любой позиции в журналах, единственным ограничением является то, что перед его ассоциированным Func: Finally #X будет отображаться Func: End #X, и оба они должны появиться перед After the wait.

Как объяснил (несколько резко) Хенк Холтерман, тот факт, что вы положили await в тело Func, означает, что после выполнения иногда все после.

Нет обходного пути as, by design вы помещаете await между Begin и End из Func.

Только мои необразованные 2 евроцента.