Асинхронная ОСАГО и «наконец» - PullRequest
18 голосов
/ 17 февраля 2011

Вот код:

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 выполняется намного позже, чем выбуду ожидать.

Какие-нибудь обходные пути?

Заранее спасибо!

Ответы [ 2 ]

13 голосов
/ 18 февраля 2011

Это отличный улов - и я согласен, что на самом деле здесь есть ошибка в CTP. Я копался в этом, и вот что происходит:

Это комбинация CTP-реализации преобразований асинхронного компилятора, а также существующего поведения TPL (Task Parallel Library) из .NET 4.0+. Вот факторы в игре:

  1. Тело finally из источника переводится в часть реального тела CLR-finally. Это желательно по многим причинам, одна из которых заключается в том, что мы можем заставить CLR выполнить его, не перехватывая / не перебрасывая исключение в дополнительное время. Это также в некоторой степени упрощает нашу генерацию кода - более простая генерация кода приводит к меньшим двоичным файлам после компиляции, что определенно желательно многими нашими клиентами. :)
  2. Общая Task для метода Func(int n) - настоящая задача TPL. Когда вы await в Consumer(), тогда остальная часть метода Consumer() фактически устанавливается как продолжение завершения Task, возвращенного из Func(int n).
  3. То, как компилятор CTP преобразует асинхронные методы, приводит к отображению return в вызов SetResult(...) до реального возврата. SetResult(...) сводится к звонку на TaskCompletionSource<>.TrySetResult.
  4. TaskCompletionSource<>.TrySetResult сигнализирует о завершении задачи TPL. Мгновенно позволяя его продолжениям происходить «когда-нибудь». Это «когда-нибудь» может означать в другом потоке, или в некоторых условиях TPL умный и говорит: «Ну, я мог бы просто вызвать его сейчас в этом же потоке».
  5. Всеохватывающий 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 и всегда ищем талантливых людей. Запись в блоге здесь с полным списком открытых позиций:)

2 голосов
/ 17 февраля 2011

Редактировать

Пожалуйста, рассмотрите ответ Тео Яунга .

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

Я не знаком с async / await, но после прочтения: Обзор 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: End #X появится перед связанным с ним Func: Finally #X, и что оба должны появиться перед After the wait.

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

Обходного пути нет: by design вы ставите await между Begin и End из Func.

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

...