Практический пример асинхронного ожидания не соответствует ожиданиям - PullRequest
2 голосов
/ 25 октября 2019

Относящийся к этому вопросу: Полностью ли блокируется поток ожидания?

[...] сначала он проверит, завершен ли вызванный метод, и если нетзарегистрирует продолжение и вернется из этого вызова метода. Позже, когда этот метод завершится, он снова войдет в конечный автомат , чтобы завершить метод

И на этот вопрос также: Когда лучшеместо для использования Task.Result вместо ожидания Task

await просто означает «этот рабочий процесс не может продолжаться до тех пор, пока эта задача не будет завершена, поэтому, если он не завершен, найдет больше работысделать и вернуться позже"

И, наконец, к этому сообщению: https://blog.stephencleary.com/2012/02/async-and-await.html

Если« ожидающий »видит, что ожидаемое еще не завершено, тоэто действует асинхронно. Он сообщает ожидающему выполнения оставшуюся часть метода по завершении, а затем возвращается из асинхронного метода. Позже , когда ожидаемое завершается, оно выполнит оставшуюся часть асинхронного метода. Если вы ожидаете встроенное ожидание (например, задачу), то остаток от асинхронного метода будет выполняться в «контексте», который был захвачен до того, как «ожидание» вернуло .

Итак, из этих сообщений я узнаю, что оператор await действительно не блокирует, но когда я попытался проверить его, я просто не могу заставить этот принцип работать так, как он утверждает, что работает. Очевидно, я что-то упускаю:

    //This will take 10 seconds
    [HttpGet("test1")]
    public async Task<TimeSpan> test()
    {
        var t1 = DateTime.Now;

        var wait1 = DoAsyncEcho("The first!", 10000);
        var wait2 = DoAsyncEcho("The second!", 10000);


        _logger.LogInformation(await wait1);
        _logger.LogInformation(await wait2);
        _logger.LogInformation("DONE!");

        var t2 = DateTime.Now;
        return t2 - t1;
    }

    //This will take 10 seconds too
    [HttpGet("test2")]
    public async Task<TimeSpan> test2()
    {
        var t1 = DateTime.Now;

        var wait1 = DoAsyncEcho("The first!", 10000);
        var wait2 = DoAsyncEcho("The second!", 10000);

        Thread.Sleep(10000);

        _logger.LogInformation(await wait1);
        _logger.LogInformation(await wait2);
        _logger.LogInformation("DONE!");

        var t2 = DateTime.Now;
        return t2 - t1;
    }


    //This will take 20
    [HttpGet("test3")]
    public async Task<TimeSpan> test3()
    {
        var t1 = DateTime.Now;

        var wait1 = await DoAsyncEcho("The first!", 10000);
        var wait2 = await DoAsyncEcho("The second!", 10000);

        _logger.LogInformation(wait1);
        _logger.LogInformation(wait2);
        _logger.LogInformation("DONE!");

        var t2 = DateTime.Now;
        return t2 - t1;
    }



    //This will take 30
    [HttpGet("test4")]
    public async Task<TimeSpan> test4()
    {
        var t1 = DateTime.Now;

        var wait1 = await DoAsyncEcho("The first!", 10000);
        var wait2 = await DoAsyncEcho("The second!", 10000);

        Thread.Sleep(10000);

        _logger.LogInformation(wait1);
        _logger.LogInformation(wait2);
        _logger.LogInformation("DONE!");

        var t2 = DateTime.Now;
        return t2 - t1;
    }

    private Task<string> DoAsyncEcho(string v, int t)
    {
        return Task<string>.Factory.StartNew(() =>
            {
                Thread.Sleep(t);
                return v;
            }
        );
    }

Как я вижу из методов test3 и test4, await действительно ждет, он не входит в конечный автомат и выполняет обратный вызовпозже, потому что он ждет полные 10 секунд первого DoAsyncEcho, а затем еще 10 секунд при втором вызове. В методах test1 и test2 время выполнения длится 10 с, так как код не ожидает возврата DoAsyncEcho, а только ждет результата позже. Particulary test2 метод спит 3 вызова по 10 секунд параллельно, в конце концов, это всего лишь 10 секунд.

Что мне здесь не хватает?

Ответы [ 2 ]

4 голосов
/ 25 октября 2019

Я думаю, что лучший способ продемонстрировать это через простое приложение Windows Forms.

Создайте приложение Windows Forms по умолчанию и поместите на него 3 кнопки (называемые button1, button2 и button3.

Затем добавьте следующий код:

async void button1_Click(object sender, EventArgs e)
{
    this.Text = "[button1_Click] About to await slowMethodAsync()";
    int result = await slowMethodAsync();
    this.Text = "[button1_Click] slowMethodAsync() returned " + result;
}

void button2_Click(object sender, EventArgs e)
{
    this.Text = "[button2_Click] About to start task to call slowMethod()";
    int result = 0;

    Task.Run(() =>
    {
        result = slowMethod();
    }).ContinueWith(_ =>
    {
        this.Invoke(new Action(() =>
        {
            this.Text = "[button2_Click] slowMethod() returned " + result;
        }));
    });
}

void button3_Click(object sender, EventArgs e)
{
    this.Text = "[button3_Click] About to call slowMethod()";
    int result = slowMethod();
    this.Text = "[button3_Click] slowMethod() returned " + result;
}

static async Task<int> slowMethodAsync()
{
    await Task.Delay(5000);
    return 42;
}

static int slowMethod()
{
    Thread.Sleep(5000);
    return 42;
}

Если вы попробуете этот код, вы заметите следующее:

Нажатие кнопки 1 немедленно изменит название на [button1_Click] About to await Task.Delay(5000), и вы можете изменить размер диалогового окна, ожидая в течение 5 секунд, после чего заголовок изменится на [button1_Click] Awaited Task.Delay(5000).

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

(Фактический код для await в действительности совсем другой, но основной механизм использования продолжения -то есть ContinueWith() и Invoke(), чтобы продолжить выполнение кода после того, как await в потоке пользовательского интерфейса иллюстрирует его подход.)

Код для button3 полностью блокирует duriНапример, Thread.Sleep(), и если вы нажимаете кнопку 3, пользовательский интерфейс полностью блокируется на 5 секунд.


Чтобы проиллюстрировать, что происходит с примером без пользовательского интерфейса, рассмотрите следующее консольное приложение:

using System;
using System.Threading;
using System.Threading.Tasks;

namespace Demo
{
    static class Program
    {
        static async Task Main()
        {
            Console.WriteLine("Main thread ID = " + Thread.CurrentThread.ManagedThreadId);

            int result = slowMethod();

            Console.WriteLine("result = " + result);
            Console.WriteLine("After calling slowMethod(), thread ID = " + Thread.CurrentThread.ManagedThreadId);

            result = await slowMethodAsync();
            Console.WriteLine("result = " + result);
            Console.WriteLine("After calling slowMethodAsync(), thread ID = " + Thread.CurrentThread.ManagedThreadId);
        }

        static async Task<int> slowMethodAsync()
        {
            await Task.Delay(5000);
            return 42;
        }

        static int slowMethod()
        {
            Thread.Sleep(5000);
            return 42;
        }
    }
}

Если вы запустите это, вы увидите вывод, подобный следующему:

Main thread ID = 1
result = 42
After calling slowMethod(), thread ID = 1
result = 42
After calling slowMethodAsync(), thread ID = 4

Обратите внимание, как код возобновился в другом потоке после ожидания.

Ключевым моментом для понимания является то, что в отношении вызывающего кода y = await X(); не возвращается до тех пор, пока не получит возвращаемое значение, а код, который запускается впоследствии, может выполняться в другом потоке.

ЭффектЭто означает, что с точки зрения блокировки THREADS вызывающий поток освобождается, чтобы завершить работу и выполнить какой-то другой код, а другой поток требуется только при возврате метода async.

Во многих случаях это означает, чточто дополнительный поток не требуется (для продолжения), и во всех случаях это означает, что исходный вызывающий поток не заблокирован и может быть освобожден для нас до пула потокове для другой задачи.

Это «не блокирующая» часть всего этого.

Для хорошего подробного объяснения того, почему иногда не требуется дополнительная нить, прочитайте превосходную статью Стивена Клири "Нет темы" .

0 голосов
/ 25 октября 2019

Похоже, вы путаете две разные интерпретации wait и block. Цель асинхронного кода - заблокировать ваш код , в то время как поток остается разблокированным. Если вы не хотите блокировать свой код , решение простое: не используйте await. Но если вы не заблокируете свой код, вы не сможете использовать результат асинхронной операции, потому что асинхронная операция выполняется одновременно с вашим кодом.

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

Вам нужен поток, чтобы оставаться разблокированным, чтобы он продолжал выполнять сообщение UIнасос, который держит ваше приложение отзывчивым. Только потому, что ваш код заблокирован, ваше приложение также не должно блокироваться. Для приложений ASP.NET вам необходимо , чтобы поток оставался разблокированным, чтобы он мог обслуживать другие входящие веб-запросы. Чем меньше потоков вы блокируете, тем больше запросов вы можете обслуживать. В этом случае async/await становится усилителем масштабируемости.

...