Async/await не реагирует должным образом

Используя приведенный ниже код, я ожидаю, что строка «Готово» появится перед «Готово» на консоли. Может ли кто-нибудь объяснить мне, почему await не будет ждать завершения задачи в этом примере?

    static void Main(string[] args)
    {
        TestAsync();
        Console.WriteLine("Ready!");
        Console.ReadKey();
    }

    private async static void TestAsync()
    {
        await DoSomething();
        Console.WriteLine("Finished");
    }

    private static Task DoSomething()
    {
        var ret = Task.Run(() =>
            {
                for (int i = 1; i < 10; i++)
                {
                    Thread.Sleep(100);
                }
            });
        return ret;
    }

person Alexander Schmidt    schedule 25.10.2011    source источник
comment
В приложении консольного режима нет поставщика синхронизации. Попробуйте это в приложении Winforms или WPF.   -  person Hans Passant    schedule 25.10.2011
comment
Можете ли вы предоставить ссылку для этого? Никогда не видел ничего о провайдерах в await/async-Demo. Почему мой образец работает асинхронно? Его просто не ждут ожидания.   -  person Alexander Schmidt    schedule 25.10.2011


Ответы (3)


Причина, по которой вы видите "Готово" после "Готово!" происходит из-за общей путаницы с асинхронными методами и не имеет ничего общего с SynchronizationContexts. SynchronizationContext контролирует, в каком потоке выполняется что-то, но «асинхронный» имеет свои очень специфические правила упорядочения. Иначе программы сошли бы с ума! :)

'await' гарантирует, что остальная часть кода в текущем асинхронном методе не будет выполняться до тех пор, пока не завершится ожидаемая вещь. Он ничего не обещает о звонящем.

Ваш асинхронный метод возвращает 'void', который предназначен для асинхронных методов, которые не позволяют исходному вызывающему объекту полагаться на завершение метода. Если вы хотите, чтобы ваш вызывающий объект также ждал, вам нужно убедиться, что ваш асинхронный метод возвращает Task (в случае, если вы хотите, чтобы наблюдались только завершение/исключения) или Task<T>, если вы действительно хотите также вернуть значение. Если вы объявите возвращаемый тип метода одним из этих двух, то компилятор позаботится обо всем остальном, о создании задачи, представляющей вызов этого метода.

Например:

static void Main(string[] args)
{
    Console.WriteLine("A");

    // in .NET, Main() must be 'void', and the program terminates after
    // Main() returns. Thus we have to do an old fashioned Wait() here.
    OuterAsync().Wait();

    Console.WriteLine("K");
    Console.ReadKey();
}

static async Task OuterAsync()
{
    Console.WriteLine("B");
    await MiddleAsync();
    Console.WriteLine("J");
}

static async Task MiddleAsync()
{
    Console.WriteLine("C");
    await InnerAsync();
    Console.WriteLine("I");
}

static async Task InnerAsync()
{
    Console.WriteLine("D");
    await DoSomething();
    Console.WriteLine("H");
}

private static Task DoSomething()
{
    Console.WriteLine("E");
    return Task.Run(() =>
        {
            Console.WriteLine("F");
            for (int i = 1; i < 10; i++)
            {
                Thread.Sleep(100);
            }
            Console.WriteLine("G");
        });
}

В приведенном выше коде буквы от «А» до «К» будут напечатаны по порядку. Вот что происходит:

«A»: до того, как будет вызвано что-либо еще

"B": OuterAsync() вызывается, Main() все еще ожидает.

"C": вызывается MiddleAsync(), OuterAsync() все еще ожидает завершения выполнения MiddleAsync().

"D": InnerAsync() вызывается, MiddleAsync() все еще ожидает завершения InnerAsync().

«E»: DoSomething() вызывается, InnerAsync() все еще ожидает завершения DoSomething(). Он немедленно возвращает задачу, которая запускается параллельно.

Из-за параллелизма существует гонка между InnerAsync(), завершающим проверку полноты задачи, возвращаемой DoSomething(), и фактическим запуском задачи DoSomething().

Как только DoSomething() запускается, он выводит «F», а затем засыпает на секунду.

В то же время, если планирование потоков не слишком запутано, InnerAsync() теперь почти наверняка поняла, что DoSomething() еще не завершена. Теперь начинается асинхронная магия.

InnerAsync() отключается от стека вызовов и сообщает, что его задача не завершена. Это заставляет MiddleAsync() отключиться от стека вызовов и сказать, что его собственная задача не завершена. Это приводит к тому, что OuterAsync() отключается от стека вызовов и сообщает, что его задача также не завершена.

Задача возвращается в Main(), который замечает, что она не завершена, и начинается вызов Wait().

тем временем...

В этом параллельном потоке задача TPL старого стиля, созданная в DoSomething(), в конце концов перестает спать. Он печатает «Г».

Как только эта задача помечается как выполненная, остальная часть InnerAsync() назначается TPL для повторного выполнения и выводит «H». Это завершает задачу, первоначально возвращенную InnerAsync().

Как только эта задача помечается как выполненная, остальная часть MiddleAsync() назначается TPL для повторного выполнения и выводит «I». Это завершает задачу, первоначально возвращенную MiddleAsync().

Как только эта задача помечается как выполненная, остальные функции OuterAsync() планируются в TPL для повторного выполнения и выводят "J". Это завершает задачу, первоначально возвращенную OuterAsync().

Поскольку задача OuterAsync() завершена, вызов Wait() возвращается, а Main() выводит «K».

Таким образом, даже с небольшим количеством параллелизма в порядке, асинхронный режим C# 5 по-прежнему гарантирует, что запись в консоль происходит именно в этом порядке.

Дайте мне знать, если это все еще кажется запутанным :)

person Theo Yaung    schedule 26.10.2011
comment
Большое спасибо. Это сделало работу. Я просто слишком боялся делать старомодные вещи ожидания :-). - person Alexander Schmidt; 26.10.2011
comment
Wait следует избегать, так как он заключает любые исключения в AggregateException; это пережиток TPL. Либо используйте SynchronizationContext для ожидания TestAsync, либо используйте TestAsync().GetAwaiter().GetResult(). - person Stephen Cleary; 26.10.2011
comment
Согласен - .GetAwaiter().GetResult() лучше старомодного .Wait() в большинстве случаев. :) Я просто хотел выбрать любой блокирующий вызов для этого примера, и мне показалось, что вникать в тонкости между ними - это отдельная тема :) - person Theo Yaung; 27.10.2011

Вы находитесь в консольном приложении, поэтому у вас нет специализированного SynchronizationContext, что означает, что ваши продолжения будут выполняться в потоках пула потоков.

Кроме того, вы не ожидаете вызова TestAsync() в Main. Это означает, что при выполнении этой строки:

await DoSomething();

метод TestAsync возвращает управление методу Main, который просто продолжает выполняться в обычном режиме, т. е. выводит «Готово!» и ждет нажатия клавиши.

Между тем, секундой позже, когда DoSomething завершится, await в TestAsync продолжит работу в потоке пула потоков и выведет «Готово».

person Nick Butler    schedule 25.10.2011

Как отмечали другие, консольные программы используют по умолчанию SynchronizationContext, поэтому продолжения, созданные await, планируются в пул потоков.

Вы можете использовать AsyncContext из моей библиотеки Nito.AsyncEx, чтобы предоставить простой асинхронный контекст :

static void Main(string[] args)
{
  Nito.AsyncEx.AsyncContext.Run(TestAsync);
  Console.WriteLine("Ready!");
  Console.ReadKey();
}

Также см. этот связанный вопрос.

person Stephen Cleary    schedule 25.10.2011
comment
Хороший намек. Мне просто не очень нравится использовать библиотеки для такой новой технологии, но, с другой стороны, я знаю, как изобретать велосипед. Спасибо! - person Alexander Schmidt; 26.10.2011