Асинхронное ожидание не реагирует как ожидалось - PullRequest
6 голосов
/ 25 октября 2011

Используя приведенный ниже код, я ожидаю, что строка «Finished» появится перед «Ready» на консоли. Может ли кто-нибудь объяснить мне, почему 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;
    }

Ответы [ 3 ]

28 голосов
/ 26 октября 2011

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

'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" до "K" будут распечатаны в порядке. Вот что происходит:

«А»: прежде чем что-либо еще будет вызвано

«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 (), которая замечает, что она не завершена, и начинается ожидание ().

Тем временем ...

В этом параллельном потоке задание TPL старого стиля, созданное в DoSomething (), в конечном итоге завершает спящий режим. Он печатает "G".

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

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

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

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

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

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

16 голосов
/ 25 октября 2011

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

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

await DoSomething();

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

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

1 голос
/ 25 октября 2011

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

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

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

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

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...