Сообщение Re[5]: Блокирующий вызов, базовый вопрос по async\await от 20.11.2019 11:30
Изменено 20.11.2019 11:34 fmiracle
Re[5]: Блокирующий вызов, базовый вопрос по async\await
Здравствуйте, Sharov, Вы писали:
S>Тут у меня проблемы и вопроса как такового нет. Суть не в том, что мы получаем, суть в том, что мои ожидания(неблокирующая обработка) не совпала с реальностью.
await — это не неблокирующая обработка, это неблокирующее ожидание.
"Обычное" блокирующее ожидание это два живых потока, один ждет другого.
Async/await же ожидание делается путем модификации кода так, что запускается обработка в некотором потоке, а код, который идет после await, исполняется после окончания выполнения асинхронного кода (обычно в том же потоке где крутился асинхронный код, хотя необязательно). А до того, как асинхронный код выполнится — никакого ожидающего кода попросту нет. Это только Таsk, который запланирован запуститься когда-то, когда закончится тот асинхронный код. Как обработчик события при асинхронном методе.
Если в "обычном" блокирующем ожидании в первом потоке есть код "до ожидания" и "код после ожидания", то в случае async/await код метода компилируется иначе — в первом потоке остается только код "до ожидания", а "код после ожидания" запускается отдельно после собственно окончания ожидания.
Рассмотрим:
Грубо говоря (без погружения в детали работы шедулеров) этот код будет обрабатываться так:
1. Выполнить в текущем потоке Р1
2. Запланировать выполнение задания для P2 в отдельном потоке, а в текущем потоке ЗАКОНЧИТЬ ВЫПОЛНЕНИЕ ЭТОГО МЕТОДА (включая выход из цикла, ага) и вернуться в вызвавший код (в твоем случае это будет в первый раз Main где придем на ReadLine, а в последующих см. дальше). (*)
3. Когда Р2 закончится — в том же потоке что работал P2 продолжить выполнение P3. А это цикл и повторение пунктов 1-3, где в случае 2 уже переходим на вот этот же поток. И кстати да, это прямой аналог ContinueWith — продолжить в том же потоке выполнение следующего Task (при базовых настройках шедулера и все равно на его усмотрение — может быть создан и отдельный поток, но "обычно и по умолчанию" используется текущий).
Т.е. схема по потокам грубо будет выглядеть так:
T1: P1->done
T2: P2->P3->done
(*)При этом создание и постановка ее в планирование задачи идет первым делом и если текущий поток на этом заканчивается (как это будет во всех твоих итерациях цикла после первой), то может получиться и так, что для выполнения P2 будет он же и использован. Может он же может не он же, как повезет.
Если приложение дополнить выводом ид потока, то оно будет наглядно видно:
Вывод:
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" );
} );
}
Вывод:
Обрати внимание на первые три строки — главный поток не ожидает первого await.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
Re[5]: Блокирующий вызов, базовый вопрос по async\await
Здравствуйте, Sharov, Вы писали:
S>Тут у меня проблемы и вопроса как такового нет. Суть не в том, что мы получаем, суть в том, что мои ожидания(неблокирующая обработка) не совпала с реальностью.
await — это не неблокирующая обработка, это неблокирующее ожидание. Но именно — ожидание.
"Обычное" блокирующее ожидание это два живых потока, один ждет другого.
Async/await же ожидание делается путем модификации кода так, что запускается обработка в некотором потоке, а код, который идет после await, исполняется после окончания выполнения асинхронного кода (обычно в том же потоке где крутился асинхронный код, хотя необязательно). А до того, как асинхронный код выполнится — никакого ожидающего кода попросту нет. Это только Таsk, который запланирован запуститься когда-то, когда закончится тот асинхронный код. Как обработчик события при асинхронном методе.
Если в "обычном" блокирующем ожидании в первом потоке есть код "до ожидания" и "код после ожидания", то в случае async/await код метода компилируется иначе — в первом потоке остается только код "до ожидания", а "код после ожидания" запускается отдельно после собственно окончания ожидания.
Рассмотрим:
Грубо говоря (без погружения в детали работы шедулеров) этот код будет обрабатываться так:
1. Выполнить в текущем потоке Р1
2. Запланировать выполнение задания для P2 в отдельном потоке, а в текущем потоке ЗАКОНЧИТЬ ВЫПОЛНЕНИЕ ЭТОГО МЕТОДА (включая выход из цикла, ага) и вернуться в вызвавший код (в твоем случае это будет в первый раз Main где придем на ReadLine, а в последующих см. дальше). (*)
3. Когда Р2 закончится — в том же потоке что работал P2 продолжить выполнение P3. А это цикл и повторение пунктов 1-3, где в случае 2 уже переходим на вот этот же поток. И кстати да, это прямой аналог ContinueWith — продолжить в том же потоке выполнение следующего Task (при базовых настройках шедулера и все равно на его усмотрение — может быть создан и отдельный поток, но "обычно и по умолчанию" используется текущий).
Т.е. схема по потокам грубо будет выглядеть так:
T1: P1->done
T2: P2->P3->done
(*)При этом создание и постановка ее в планирование задачи идет первым делом и если текущий поток на этом заканчивается (как это будет во всех твоих итерациях цикла после первой), то может получиться и так, что для выполнения P2 будет он же и использован. Может он же может не он же, как повезет.
Если приложение дополнить выводом ид потока, то оно будет наглядно видно:
Вывод:
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" );
} );
}
Вывод:
Обрати внимание на первые три строки — главный поток не ожидает первого await.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