Информация об изменениях

Сообщение Re[5]: Блокирующий вызов, базовый вопрос по async\await от 20.11.2019 11:30

Изменено 20.11.2019 11:36 fmiracle

Re[5]: Блокирующий вызов, базовый вопрос по async\await
Здравствуйте, Sharov, Вы писали:

S>Тут у меня проблемы и вопроса как такового нет. Суть не в том, что мы получаем, суть в том, что мои ожидания(неблокирующая обработка) не совпала с реальностью.


await — это не неблокирующая обработка, это неблокирующее ожидание. Но именно — ожидание.

"Обычное" блокирующее ожидание это два живых потока, один ждет другого.
Async/await же ожидание делается путем модификации кода так, что запускается обработка в некотором потоке, а код, который идет после await, исполняется после окончания выполнения асинхронного кода (обычно в том же потоке где крутился асинхронный код, хотя необязательно). А до того, как асинхронный код выполнится — никакого ожидающего кода попросту нет. Это только Таsk, который запланирован запуститься когда-то, когда закончится тот асинхронный код. Как обработчик события при асинхронном методе.
Если в "обычном" блокирующем ожидании в первом потоке есть код "до ожидания" и "код после ожидания", то в случае async/await код метода компилируется иначе — в первом потоке остается только код "до ожидания", а "код после ожидания" запускается отдельно после собственно окончания ожидания.

Рассмотрим:

Console.WriteLine(1); // P1
await ProcessTaskAsync(); // P2
Console.WriteLine(3); // P3


Грубо говоря (без погружения в детали работы шедулеров) этот код будет обрабатываться так:
1. Выполнить в текущем потоке Р1
2. Запланировать выполнение задания для P2 в отдельном потоке, а в текущем потоке ЗАКОНЧИТЬ ВЫПОЛНЕНИЕ ЭТОГО МЕТОДА (включая выход из цикла, ага) и вернуться в вызвавший код (в твоем случае это будет в первый раз Main где придем на ReadLine, а в последующих см. дальше). (*)
3. Когда Р2 закончится — в том же потоке что работал P2 продолжить выполнение P3. А это цикл и повторение пунктов 1-3, где в случае 2 уже переходим на вот этот же поток. И кстати да, это прямой аналог ContinueWith — продолжить в том же потоке выполнение следующего Task (при базовых настройках шедулера и все равно на его усмотрение — может быть создан и отдельный поток, но "обычно и по умолчанию" используется текущий).

Т.е. схема по потокам грубо будет выглядеть так:
T1: P1->done
T2: P2->P3->done

(*)При этом создание и постановка ее в планирование задачи идет первым делом и если текущий поток на этом заканчивается (как это будет во всех твоих итерациях цикла после первой), то может получиться и так, что для выполнения P2 будет он же и использован. Может он же может не он же, как повезет.

Если приложение дополнить выводом ид потока, то оно будет наглядно видно:
private static void Main( string[] args )
        {
            Console.WriteLine( $"T{Thread.CurrentThread.ManagedThreadId}: start" );
            Test();
            Console.WriteLine( $"T{Thread.CurrentThread.ManagedThreadId}: returned to main, wait for readline" );
            Console.ReadLine();
        }

        private static async void Test()
        {

            while( true )
            {
                Console.WriteLine( $"T{Thread.CurrentThread.ManagedThreadId}: 1" );
                await ProcessTaskAsync();
                Console.WriteLine( $"T{Thread.CurrentThread.ManagedThreadId}: 3" );

            }
        }

        private static Task ProcessTaskAsync()
        {
            return Task.Factory.StartNew( () =>
            {
                Thread.Sleep( 1000 );
                Console.WriteLine( $"T{Thread.CurrentThread.ManagedThreadId}: 2" );
            } );
        }


Вывод:

T1: start
T1: 1
T1: returned to main, wait for readline
T3: 2
T3: 3
T3: 1
T4: 2
T4: 3
T4: 1
T3: 2
T3: 3
T3: 1
T3: 2
T3: 3
T3: 1
T5: 2
T5: 3
T5: 1
T5: 2
T5: 3
T5: 1

Обрати внимание на первые три строки — главный поток не ожидает первого await.
Re[5]: Блокирующий вызов, базовый вопрос по async\await
Здравствуйте, Sharov, Вы писали:

S>Тут у меня проблемы и вопроса как такового нет. Суть не в том, что мы получаем, суть в том, что мои ожидания(неблокирующая обработка) не совпала с реальностью.


await — это не неблокирующая обработка, это неблокирующее ожидание. Но именно — ожидание.

"Обычное" блокирующее ожидание это два живых потока, один ждет другого.
Async/await же ожидание делается путем модификации кода так, что запускается обработка в некотором потоке, а код, который идет после await, исполняется после окончания выполнения асинхронного кода (обычно в том же потоке где крутился асинхронный код, хотя необязательно). А до того, как асинхронный код выполнится — никакого ожидающего кода попросту нет. Это только Таsk, который запланирован запуститься когда-то, когда закончится тот асинхронный код. Как обработчик события при асинхронном методе.
Если в "обычном" блокирующем ожидании в первом потоке есть код "до ожидания" и "код после ожидания", то в случае async/await код метода компилируется иначе — в первом потоке остается только код "до ожидания", а "код после ожидания" запускается отдельно после собственно окончания ожидания.

Рассмотрим:

Console.WriteLine(1); // P1
await ProcessTaskAsync(); // P2
Console.WriteLine(3); // P3


Грубо говоря (без погружения в детали работы шедулеров) этот код будет обрабатываться так:
1. Выполнить в текущем потоке Р1
2. Запланировать выполнение задания для P2 в отдельном потоке, а в текущем потоке ЗАКОНЧИТЬ ВЫПОЛНЕНИЕ ЭТОГО МЕТОДА (включая выход из цикла, ага) и вернуться в вызвавший код (в твоем случае это будет в первый раз Main где придем на ReadLine, а в последующих см. дальше). (*)
3. Когда Р2 закончится — в том же потоке что работал P2 продолжить выполнение P3. А это цикл и повторение пунктов 1-3, где в случае 2 уже переходим на вот этот же поток. И кстати да, это прямой аналог ContinueWith — продолжить в том же потоке выполнение следующего Task (при базовых настройках шедулера и все равно на его усмотрение — может быть создан и отдельный поток, но "обычно и по умолчанию" используется текущий).

Т.е. схема по потокам грубо будет выглядеть так:
T1: P1->done
T2: P2->P3->done

(*)При этом создание и постановка ее в планирование задачи идет первым делом и если текущий поток на этом заканчивается (как это будет во всех твоих итерациях цикла после первой), то может получиться и так, что для выполнения P2 будет он же и использован. Может он же может не он же, как повезет.

Если приложение дополнить выводом ид потока, то оно будет наглядно видно:
private static void Main( string[] args )
        {
            Console.WriteLine( $"T{Thread.CurrentThread.ManagedThreadId}: start" );
            Test();
            Console.WriteLine( $"T{Thread.CurrentThread.ManagedThreadId}: returned to main, wait for readline" );
            Console.ReadLine();
        }

        private static async void Test()
        {

            while( true )
            {
                Console.WriteLine( $"T{Thread.CurrentThread.ManagedThreadId}: 1" );
                await ProcessTaskAsync();
                Console.WriteLine( $"T{Thread.CurrentThread.ManagedThreadId}: 3" );

            }
        }

        private static Task ProcessTaskAsync()
        {
            return Task.Factory.StartNew( () =>
            {
                Thread.Sleep( 1000 );
                Console.WriteLine( $"T{Thread.CurrentThread.ManagedThreadId}: 2" );
            } );
        }


Вывод:

T1: start
T1: 1
T1: returned to main, wait for readline
T3: 2
T3: 3
T3: 1
T4: 2
T4: 3
T4: 1
T3: 2
T3: 3
T3: 1
T3: 2
T3: 3
T3: 1
T5: 2
T5: 3
T5: 1
T5: 2
T5: 3
T5: 1

Обрати внимание на первые три строки — главный поток не блокируется на первом же await. И даже не ждет его — код, запланированный после await выполняется уже в другом потоке после окончания первого await.