Блокирующий вызов, базовый вопрос по async\await
От: Sharov Россия  
Дата: 19.11.19 11:19
Оценка:
Здравтсвуйте.

Наверняка подобный вопрос(задачку) здесь разобрали лет 5-6 назад, если не ранее. Я как всегда вовремя

С удивлением узнал как работает сл. код:
 private static void Main(string[] args)
        {
            Test();
            Console.ReadLine();
        }


        private static async void Test()
        {
            
            while (true)
            {
                Console.WriteLine(1);
                await ProcessTaskAsync();
                Console.WriteLine(3);

            }
        }

        private static Task ProcessTaskAsync()
        {
            return Task.Factory.StartNew(() =>
            {
                Thread.Sleep(1000);
                Console.WriteLine(2);
            });
        }


Все время будет печать 1 2 3(по строчно). Я слегка офигел, т.к. был в полной уверенности, что код
await ProcessTaskAsync();
Console.WriteLine(3);


неблокирующий. Т.е. создаем поток для таски, там ее выполняем, цеплякм continuation
Console.WriteLine(3)

и возврашаемся тут же в цикл. Ожидал вот это 11111111111111....1231111111111111111111111111111...23......

А фигушки, оказывается, что продолжение цикла это тоже ведь continuation, поэтому данный код у нас блокирующий. Очень интуитивное поведение
Я ожидал, что будет что-то вроде
while (true)
{
  Console.WriteLine(1);
  var tsk = ProcessTaskAsync()
   tsk.ContinueWith(t=> Console.WriteLine(3);)

}


Т.е. неблокирующий сервер таким образом через async\await не напишешь. Век живи, век учись.
Кодом людям нужно помогать!
Re: Блокирующий вызов, базовый вопрос по async\await
От: Qulac Россия  
Дата: 19.11.19 12:00
Оценка:
Здравствуйте, Sharov, Вы писали:

S>Здравтсвуйте.


S>Наверняка подобный вопрос(задачку) здесь разобрали лет 5-6 назад, если не ранее. Я как всегда вовремя


S>С удивлением узнал как работает сл. код:

S>
S> private static void Main(string[] args)
S>        {
S>            Test();
S>            Console.ReadLine();
S>        }


S>        private static async void Test()
S>        {
            
S>            while (true)
S>            {
S>                Console.WriteLine(1);
S>                await ProcessTaskAsync();
S>                Console.WriteLine(3);

S>            }
S>        }

S>        private static Task ProcessTaskAsync()
S>        {
S>            return Task.Factory.StartNew(() =>
S>            {
S>                Thread.Sleep(1000);
S>                Console.WriteLine(2);
S>            });
S>        }
S>


S>Все время будет печать 1 2 3(по строчно). Я слегка офигел, т.к. был в полной уверенности, что код

S>
S>await ProcessTaskAsync();
S>Console.WriteLine(3);
S>


S>неблокирующий. Т.е. создаем поток для таски, там ее выполняем, цеплякм continuation

S>
S>Console.WriteLine(3)
S>

S> и возврашаемся тут же в цикл. Ожидал вот это 11111111111111....1231111111111111111111111111111...23......

S>А фигушки, оказывается, что продолжение цикла это тоже ведь continuation, поэтому данный код у нас блокирующий. Очень интуитивное поведение

S>Я ожидал, что будет что-то вроде
S>
S>while (true)
S>{
S>  Console.WriteLine(1);
S>  var tsk = ProcessTaskAsync()
S>   tsk.ContinueWith(t=> Console.WriteLine(3);)

S>}
S>


S>Т.е. неблокирующий сервер таким образом через async\await не напишешь. Век живи, век учись.




await ProcessTaskAsync();


А он вообще нужен здесь await?
Программа – это мысли спрессованные в код
Re[2]: Блокирующий вызов, базовый вопрос по async\await
От: Sharov Россия  
Дата: 19.11.19 12:19
Оценка:
Здравствуйте, Qulac, Вы писали:

Q>А он вообще нужен здесь await?


Ну да, Console.WriteLine(3); должна выполняться сразу после, они могут быть как-то связаны -- освобождение ресурсов и т.д.
Я выше описал на тасках как думал, что этот код будет работать. А оно эвана как...
Кодом людям нужно помогать!
Re: Блокирующий вызов, базовый вопрос по async\await
От: Sinclair Россия https://github.com/evilguest/
Дата: 19.11.19 12:21
Оценка: 12 (2) +3
Здравствуйте, Sharov, Вы писали:

S>Здравтсвуйте.


S>Наверняка подобный вопрос(задачку) здесь разобрали лет 5-6 назад, если не ранее. Я как всегда вовремя


S>С удивлением узнал как работает сл. код:

S>
S> private static void Main(string[] args)
S>        {
S>            Test();
S>            Console.ReadLine();
S>        }


S>        private static async void Test()
S>        {
            
S>            while (true)
S>            {
S>                Console.WriteLine(1);
S>                await ProcessTaskAsync();
S>                Console.WriteLine(3);

S>            }
S>        }

S>        private static Task ProcessTaskAsync()
S>        {
S>            return Task.Factory.StartNew(() =>
S>            {
S>                Thread.Sleep(1000);
S>                Console.WriteLine(2);
S>            });
S>        }
S>


S>Все время будет печать 1 2 3(по строчно). Я слегка офигел, т.к. был в полной уверенности, что код

S>
S>await ProcessTaskAsync();
S>Console.WriteLine(3);
S>


S>неблокирующий. Т.е. создаем поток для таски, там ее выполняем, цеплякм continuation

S>
S>Console.WriteLine(3)
S>

S> и возврашаемся тут же в цикл. Ожидал вот это 11111111111111....1231111111111111111111111111111...23......

Как раз нет. Ты же сам сказал — подождать (await). Весь смысл такого кода — возможность писать кооперативную многозадачность.
Т.е. твой код выглядит (и работает) как синхронный однопоточный; но при этом он не замораживает физический поток на время ProcessTaskAsync(). В пуле потоков не болтаются спящие потоки, тормозя шедулер; в адресном пространстве процесса не сожрано по 1 мегабайту на стек каждой такой "спящей красавицы", всё такое. Нет затрат на переключение контекстов в момент когда ты делаешь await — usermode шедулер, который исполняет твой Test(), берёт следующий Task, готовый к выполнению, и шарашит его.
Уйдемте отсюда, Румата! У вас слишком богатые погреба.
Re[2]: Блокирующий вызов, базовый вопрос по async\await
От: Sharov Россия  
Дата: 19.11.19 12:44
Оценка:
Здравствуйте, Sinclair, Вы писали:

S>Как раз нет. Ты же сам сказал — подождать (await). Весь смысл такого кода — возможность писать кооперативную многозадачность.


Я всегда стараюсь явно писать все через таски, поэтому async\await старался избегать. Мне казалось, что суть await в данном месте это подождать в отдельном,уже созданном для ProcessTask, потоке результата операции
и продолжить его с кодом после await. Причем все это в потоке отдельном от потока while(true). Ровно так, как я описал через task.

S>В пуле потоков не болтаются спящие потоки, тормозя шедулер;


А как они его тормозят? Ну спят и спят, возьмет не спящий. А вот если таких нет, то придется создавать.
Кодом людям нужно помогать!
Re[3]: Блокирующий вызов, базовый вопрос по async\await
От: Sinclair Россия https://github.com/evilguest/
Дата: 19.11.19 12:59
Оценка: +1
Здравствуйте, Sharov, Вы писали:

S>А как они его тормозят? Ну спят и спят, возьмет не спящий. А вот если таких нет, то придется создавать.

При каждом переключении, шедулер ОС принимает решение о том, кому отдать квант времени.
Разработчики ядер много работают, чтобы затраты на принятие такого решения были минимальны и не катастрофически зависели от количества имеющихся потоков.
Тем не менее, чудес и алгоритмов за O(1) не бывает. Неактивные потоки — ждущие на примитивах синхронизации или на Sleep() — занимают место в списке потоков, и замедляют переключение.
Где-то была в своё время статья про то, сколько ресурсов тратится на собственно "шедулинг"; для любого процессора и любого 0<e<100% существует конечное N такое, что при регистрации N спящих потоков в системе на работу единственного неспящего будет выделяться не более e мощности процессора.
Уйдемте отсюда, Румата! У вас слишком богатые погреба.
Re[4]: Блокирующий вызов, базовый вопрос по async\await
От: Sharov Россия  
Дата: 19.11.19 13:03
Оценка:
Здравствуйте, Sinclair, Вы писали:

S>>А как они его тормозят? Ну спят и спят, возьмет не спящий. А вот если таких нет, то придется создавать.

S>При каждом переключении, шедулер ОС принимает решение о том, кому отдать квант времени.
S>Разработчики ядер много работают, чтобы затраты на принятие такого решения были минимальны и не катастрофически зависели от количества имеющихся потоков.
S>Тем не менее, чудес и алгоритмов за O(1) не бывает. Неактивные потоки — ждущие на примитивах синхронизации или на Sleep() — занимают место в списке потоков, и замедляют переключение.

Я так и думал, просто мне казалось что расход на сотни, край тысячи таких потоков по нашим дням дело плевое. Расход в смысле O(n). А так да, согласен.
Кодом людям нужно помогать!
Re[5]: Блокирующий вызов, базовый вопрос по async\await
От: paradoks  
Дата: 19.11.19 13:43
Оценка:
Здравствуйте, Sharov, Вы писали:

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

тем не менее код работает ожидаемо никаких неправильностей не вижу


ввод автора что аинхронный сервер написать на асинхронных и авэйтах нельз конечно ошибочен
и главное вовсе не следует из кода что автор привел!
Из авторского кода наоборот следует что все работает как надо и пригодно к задаче.
Отредактировано 19.11.2019 13:53 paradoks . Предыдущая версия . Еще …
Отредактировано 19.11.2019 13:51 paradoks . Предыдущая версия .
Re[6]: Блокирующий вызов, базовый вопрос по async\await
От: Sharov Россия  
Дата: 19.11.19 14:01
Оценка:
Здравствуйте, paradoks, Вы писали:

P>тем не менее код работает ожидаемо никаких неправильностей не вижу


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

P>ввод автора что аинхронный сервер написать на асинхронных и авэйтах нельз конечно ошибочен


Такого вывода, скорее это был вопрос без вопросительного знака.
Кодом людям нужно помогать!
Re[2]: Блокирующий вызов, базовый вопрос по async\await
От: Sharov Россия  
Дата: 19.11.19 14:06
Оценка:
Здравствуйте, Sinclair, Вы писали:

S>Как раз нет. Ты же сам сказал — подождать (await). Весь смысл такого кода — возможность писать кооперативную многозадачность.


Да, но я предполагал, что семантика будет а-ля tsk.ContinueWith. Отдельно от потока цилка. Тут главное понять было, что
сл. итерации цикла это ведь тоже continuation, наряду с Console.WriteLine(3).
Кодом людям нужно помогать!
Re[3]: Блокирующий вызов, базовый вопрос по async\await
От: alexzzzz  
Дата: 19.11.19 14:13
Оценка: 8 (1)
Здравствуйте, Sharov, Вы писали:

S>Мне казалось, что суть await в данном месте это подождать в отдельном, уже созданном для ProcessTask, потоке результата операции

S>и продолжить его с кодом после await.

Как-то так оно и работает. Ты вызываешь асинхронный Test, он выполняется до первого await и возвращает управление. В результате "главный" поток печатает
1
La fin

и переходит дальше на ReadLine, где и останавливается в ожидании ввода. А то, что мы авейтим и всё остальное после него, крутится в это время в каком-то другом потоке/потоках. Поскольку приложение консольное и контекст синхронизации у главного потока не задан (его SynchronizationContext.Current is null), то вот это всё остальное после await в главный поток больше не вернётся, т.к. без контекста синхронизации не может.

В WinForms/WPF-приложении результат, думаю, будет сильно другой. Там главным потокам задан SynchronizationContext.

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

public class Program
{
    private static void Main(string[] args)
    {
        Test();
        Print("La fin");
        Console.ReadLine();
    }

    static async void Test()
    {
        while (true)
        {
            Print(1);
            await ProcessTaskAsync();
            Print(3);
        }
    }

    static Task ProcessTaskAsync()
    {
        return Task.Factory.StartNew(() =>
        {
            Thread.Sleep(1000);
            Print(2);
        });
    }

    static void Print(object message)
    {
        Console.WriteLine($"thread {Thread.CurrentThread.ManagedThreadId} says {message}");
    }
}
Отредактировано 19.11.2019 14:25 alexzzzz . Предыдущая версия . Еще …
Отредактировано 19.11.2019 14:18 alexzzzz . Предыдущая версия .
Отредактировано 19.11.2019 14:16 alexzzzz . Предыдущая версия .
Отредактировано 19.11.2019 14:16 alexzzzz . Предыдущая версия .
Re[7]: Блокирующий вызов, базовый вопрос по async\await
От: paradoks  
Дата: 19.11.19 14:19
Оценка:
Здравствуйте, Sharov, Вы писали:

S>Здравствуйте, paradoks, Вы писали:


P>>тем не менее код работает ожидаемо никаких неправильностей не вижу


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


но откуда такие ожидания взялись? тем более что ты САМ поставил внутри цикла БЛОК — await
Убери его, сделай все вызовы внутри блока асинхронными и цикл у тебя будет не-блокирующий.

Как можно было ожидать что цикл не заблокируется если ты его САМ заблокировал?
Ты же понимаешь что твои ожидания взялись ниоткуда и не основаны на том коде что ты напиcал?

имхо если у тебя задача обработки асинхронных входных сообщений асинхронныи функциями — то все это отлично работает
Re[8]: Блокирующий вызов, базовый вопрос по async\await
От: Sharov Россия  
Дата: 19.11.19 14:38
Оценка:
Здравствуйте, paradoks, Вы писали:


P>Как можно было ожидать что цикл не заблокируется если ты его САМ заблокировал?


Почему await==блокировке? Разве это не разделение между таском и его продолжением, синтаксический захар над
tsk = ProcessAsync(...);
tsk.ContinueWith(...)
?

P>Ты же понимаешь что твои ожидания взялись ниоткуда и не основаны на том коде что ты напиcал?


Ожидания взялись от неправильного понимания работы await. Он, видимо, возвращает управление в вызвавший его поток.
Ну т.е. разница между tsk.ContinueWith и await весьма огромна.

P>имхо если у тебя задача обработки асинхронных входных сообщений асинхронныи функциями — то все это отлично работает


Код выше синхронный, в том то и дело. Пока ProcessAsync не закончится, цикл не продолжится.
Кодом людям нужно помогать!
Re[9]: Блокирующий вызов, базовый вопрос по async\await
От: paradoks  
Дата: 19.11.19 14:47
Оценка:
Здравствуйте, Sharov, Вы писали:


S>Код выше синхронный, в том то и дело. Пока ProcessAsync не закончится, цикл не продолжится.


а в чем смысл этого?

зачем тебе ждать СИНХРОННО когда вывод на экран закончится? и при этом назвать ф-и мнемонически асинхронной...
неблокирующий асинхронный сервер что ты упоминал как раз и не должен лочиться на таких подзадачах
Отредактировано 19.11.2019 14:50 paradoks . Предыдущая версия .
Re[10]: Блокирующий вызов, базовый вопрос по async\await
От: Sharov Россия  
Дата: 19.11.19 14:56
Оценка:
Здравствуйте, paradoks, Вы писали:

P>зачем тебе ждать СИНХРОННО когда вывод на экран закончится? и при этом назвать ф-и мнемонически асинхронной...


Вот и я не ожидал, что придется ждать синхронно. Выше уже ответил почему данный код работает синхронно, т.е. блокирующий.
Кодом людям нужно помогать!
Re: Блокирующий вызов, базовый вопрос по async\await
От: BlackHorse Россия  
Дата: 20.11.19 07:48
Оценка:
Здравствуйте, Sharov, Вы писали:

А если так?
        static async void Test()
        {
            
            while (true)
            {
                Console.WriteLine(1);
                var t = ProcessTaskAsync();
                Console.WriteLine(3);
                await t;
            }
        }


Т.е. это мы в await'e явно ждём выполнения задачи.
Re[2]: Блокирующий вызов, базовый вопрос по async\await
От: Sharov Россия  
Дата: 20.11.19 09:31
Оценка:
Здравствуйте, BlackHorse, Вы писали:

BH>Здравствуйте, Sharov, Вы писали:


BH>А если так?

BH>
BH>        static async void Test()
BH>        {
            
BH>            while (true)
BH>            {
BH>                Console.WriteLine(1);
BH>                var t = ProcessTaskAsync();
BH>                Console.WriteLine(3);
BH>                await t;
BH>            }
BH>        }
BH>


BH>Т.е. это мы в await'e явно ждём выполнения задачи.


Не совсем то, должно быть(точнее, мое ожидание см. выше) 23 и неблокирующее, т.е. 11111...11123111..11123111....23 и т.д.
А тут 132132132.
Кодом людям нужно помогать!
Re[3]: Блокирующий вызов, базовый вопрос по async\await
От: BlackHorse Россия  
Дата: 20.11.19 10:10
Оценка:
Здравствуйте, Sharov, Вы писали:

S>Не совсем то, должно быть(точнее, мое ожидание см. выше) 23 и неблокирующее, т.е. 11111...11123111..11123111....23 и т.д.

S>А тут 132132132.

            while (true)
            {
                Console.WriteLine(1);
                ProcessTaskAsync();
                Console.WriteLine(3);
                
            }


а так? получаем 131313131313132...
Re[4]: Блокирующий вызов, базовый вопрос по async\await
От: Sharov Россия  
Дата: 20.11.19 10:33
Оценка:
Здравствуйте, BlackHorse, Вы писали:

BH>
BH>            while (true)
BH>            {
BH>                Console.WriteLine(1);
BH>                ProcessTaskAsync();
BH>                Console.WriteLine(3);
                
BH>            }
BH>


BH>а так? получаем 131313131313132...



Тут у меня проблемы и вопроса как такового нет. Суть не в том, что мы получаем, суть в том, что мои ожидания(неблокирующая обработка) не совпала с реальностью.
Кодом людям нужно помогать!
Re[5]: Блокирующий вызов, базовый вопрос по async\await
От: fmiracle  
Дата: 20.11.19 11:30
Оценка: 29 (3)
Здравствуйте, 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.
Отредактировано 20.11.2019 11:36 fmiracle . Предыдущая версия . Еще …
Отредактировано 20.11.2019 11:34 fmiracle . Предыдущая версия .
Подождите ...
Wait...
Пока на собственное сообщение не было ответов, его можно удалить.