Как стать автором
Обновить

Async/await. Куда делся thread.  Инверсия асинхронности. Разоблачение мистификации

Уровень сложностиСредний
Время на прочтение8 мин
Количество просмотров13K
Всего голосов 14: ↑2 и ↓12-10
Комментарии25

Комментарии 25

Нет, async придумали не для того, чтобы "экономить потоки", а для того, чтобы их не блокировать.

Нет, добавление await не превращает вызов в "синхронный". Поэтому его так никто и не называет. Если вы хотите увидеть синхронный вызов асинхронной функции, смотрите тут.

Нет, добавление await и его эффект - не таинственное знание и не "Преобразованный Асинхронный вызов", а наиболее обыденный (если угодно, "непреобразованный") способ использования асинхронных функций.

Нет, добавление await не приводит к выполнению всего кода на одном потоке. Распределением Task по потокам занимается контекст синхронизации. Самый простой способ увидеть это в вашем неправильном, подогнанном под ответ примере - добавить строчку Console.WriteLine($"TrID={Thread.CurrentThread.ManagedThreadId}"); в SomeMethodAsync1, прямо перед await Task.Yield();, что даст вам:

TrID=1
TrID=4
MethodIter=0
MethodIter=1
MethodIter=2
...

Да, я тоже заметил что пропал первый поток (ID=1), мне как раз было интересно услышать комментарий по этому поводу, вам в любом случае плюс за профессиональную внимательность.

Но исходя из определения асинхронности которое я привел, наличие дополнительного потока не определяет является вызов метода асинхронным или синхронным:

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

Потом вы явно что то здесь:

Нет, async придумали не для того, чтобы "экономить потоки", а для того, чтобы их не блокировать.

не договариваете, как минимум! Вообще-то ASYNC придумали чтобы можно было писать AWAIT. Зачем нужен AWAIT, вы почему-то скромно умолчали. А у меня речь идет именно про AWAIT. Поэтому мне кажется вы в какой-то степени исказили содержание статьи таким образом и пишите притензии к этому искаженному содержанию. Надеюсь не намеренно.

я тоже заметил что пропал первый поток (ID=1), мне как раз было интересно услышать комментарий по этому поводу

Как вы, конечно, знаете, вызов любой асинхронной функции начинается синхронно (а значит, совершенно точно на том же потоке, что делает вызов). Выполнение становится асинхронным, когда в цепочке вызовов встречается первый незавершённый await.

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

Когда у вас не было await перед SomeMethodAsync1, всё работало точно так же относительно первого незавершённого await, смена потока, выполняющего продолжение, происходила на том же await Task.Yield. Разница только в том, что без await ваши Task Main и Task SomeMethodAsync1 выполняются одновременно, а не последовательно, что вам чаще всего не надо (Fire and Forget по типу 1, плюс с неожиданным параллелизмом в силу характера работы консольного контекста синхронизации).

Вообще-то ASYNC придумали чтобы можно было писать AWAIT. Зачем нужен AWAIT, вы почему-то скромно умолчали. А у меня речь идет именно про AWAIT

Под словами "async придумали" я имел в виду "придумали механизм асинхронных вызовов в .NET", а не "придумали ключевое слово async в C#".

Ключевые слова async и await в C# (Async и Await в VB) - это часть одного синтаксического пакета, и говорить о том, что их придумали как самоцель, некорректно. Их придумали, чтобы обеспечить удобное использование механизма асинхронных вызовов из языка. Указывать на различия между ними - примерно как указывать на различия между операторами + и =.

Ключевое слово async разрешает компилятору преобразовывать тело метода в конечный автомат, а ключевое слово await указывает, в каких местах это делать. Одно бесполезно без другого, поэтому вы не можете использовать ключевое слово await в методе, который не помечен ключевым словом async. Наоборот можно, но бессмысленно, поэтому при попытке сделать это вы получаете warning от компилятора.

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

А для чего нужен механизм асинхронного вызова, я написал в самом начале - чтобы не блокировать потоки.

А для чего нужен механизм асинхронного вызова, я написал в самом начале - чтобы не блокировать потоки.

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

 Task Main и Task SomeMethodAsync1 выполняются одновременно

и когда они выполняются последовательно, кто же с этим спорит-то?

Вопрос то в том, за счет чего и каким образом исключается блокировка. И вроде как из картинок очевидно, что есть более эффективный способ избежать бокировки с asyn/await, и менее эффективный гипотетический или, можно сказать старомодный.

Получается что вы статью не прочитали или не внимательно прочитали и отвечаете на какие-то вами придуманные заблуждения.

Вопрос то в том, за счет чего и каким образом исключается блокировка. 

За счёт того, что таск - это объект. В какой-то момент управление возвращается потоку туда, где создавался таск, а таск остаётся жить сам по себе, пока какой-то поток не продолжит его выполнение. Цепочка из авейтов как раз таки позволяет сделать return из функции на любом уровне вложенности и позволить треду заниматься своими делами.

А если авейт не написать, то будет создан новый таск, который когда-нибудь будет продолжен в каком-нибудь потоке.

Всё в точном соответствии с работой примеров.

З.Ы. На самом деле я так не понял в чём именно вопрос, а статьи мне не помогли. Типа где-то кто-то не правильно написал как таски работают?

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

Task Main и Task SomeMethodAsync1 выполняются одновременно

и когда они выполняются последовательно, кто же с этим спорит-то?

Вы не то чтобы с этим спорите, вы (и в прошлой статье, и в этой) сначала говорите "проверим, блокируется ли поток", а потом показываете код, суть которого - что некий новый поток либо фигурирует, либо всё обходится без него.

В результате вы смешиваете понятия "X потоков было блокировано" и "код работал на X потоках". И пытаетесь открыть глаза собравшимся то на факт наличия блокировки потоков, как в прошлой статье, то на новый способ этого избежать, как в этой. В обоих случаях это подмена понятий, и в обоих случаях ваши примеры на самом деле иллюстрируют не то, что вы думаете.

Вы не то чтобы с этим спорите, вы (и в прошлой статье, и в этой) сначала говорите "проверим, блокируется ли поток"

Я специально поиск запустил по предыдущей статье, там нет таких слов: "проверим, блокируется ли поток", там даже просто слов "блокирует" "блок" нет.

Там ответ на вопрос который мне задали:

Может ли магия async/await сама создавать потоки или это все делается явно тем, кто асинхронные функции пишет?

Вы нервный срыв мне хотите устроить?

Вы процитируйте тогда что-то действительно мной написанное, где есть какое-то необоснованное заявление, или противоречие...

Сложна!
В смысле сложно написано. Я знаю как работает асинхронщина, но не так и не понял, что хотел сказать автор. Сорвать какие-то покровы?

Вот пример, когда всё вообще синхронно выполняется. Достаточно выкинуть Yield

Всё держится на том, что код всегда выполняется синхронно и есть некие "истинно асинхронные операции", на которых выполнение асинк кода прекращается, а поток может заняться как-то другой работой: уйти в пул или выполнять мейн дальше. Кооперативная многозадачность, типа. Такие операции и руками можно написать, если надо, асинк нужен ради сахара. Когда "истинно синхронные операция" завершится, дальнейшее выполнение подхватит какой-то поток.

Прикол в том, что для ожидания завершения "истинно асинхронной операции" не нужен никакой поток, вот вся экономия.

А как же Task.Run? ;)

А с ним что не так? Мы его не авейтим.

Вот пример, когда всё вообще синхронно выполняется. Достаточно выкинуть Yield

Любая функция выполняется синхронно, чтобы сотворить пример асинхронной функции достаточно подписать функцию async и добавить в начале await Task.Yield()

и тогда все тело такоймодифицированной функции станет выполняться асинхронно, в соответствии с определением асинхронности которое я привел в статье. То есть тело функции будет выполняться когда-то после завершения вызова такой функции. Надо хорошо понимать параметры этого когда-то и желательно уметь этими параметрами управлять. Для того чтобы разобрать эти параметры и как они управляются в зависимости от контекста достаточно даже функции со слипами (sleep - ами) для примера.

Вот до этого места со всем согласен:

Надо хорошо понимать параметры этого когда-то и желательно уметь этими параметрами управлять. Для того чтобы разобрать эти параметры и как они управляются в зависимости от контекста достаточно даже функции со слипами (sleep - ами) для примера.

А каких параметрах идёт речь?

я же написал:

То есть тело функции будет выполняться когда-то после завершения вызова такой функции.

И чего?

А каких параметрах идёт речь?

то есть вы все таки хотите поподробнее, хорошо! Это хороший вопрос чтобы ответить на него в следующей статье.

еще бы неплохо научиться использовать Task.Delay вместо Thread.Sleep в асинхронном контексте выполнения

а то неясно что тестим, блокируя заранее неизвестный тред

это собственно и было значением фразы "тут нет тредов"

про них тут неизвестно, они вынесены за рамки этого кода, так что не надо их трогать

блокирующие тред операции должны быть заботливо упакованы в асинк-методы, которые не заблочат тред их вызывающий (скажем есть foo которая слипит тред на секунду - она же его в самом примитивном виде и должна породить, вернуть управление и по завершению работы в треде - вернуть результат в таск), потому что тот в свою очередь может быть например тредом ui или еще каким синглтредовым контекстом обвязан и там все от блокирующей операции колом встанет

я второй раз прошу автора заняться изучением теории асинхронщины

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

он является, но это никак не противоречит фразе there is no thread потому что в реализации асинк-эвейтов, корутин, фьючей и прочей асинхронной дребедени нет продуцирования тредов, этим занимаются тредпулы в обвязке - контекст выполнения собственно стейт-машин асинхронщиной занимающихся, в котлин-корутинах, например их еще и наглядно можно задавать, а сама асинхронщина ближе всего к понятию "плоские коллбеки", которые и есть этот ваш тру-асинк

еще бы неплохо научиться использовать Task.Delay вместо Thread.Sleep в асинхронном контексте выполнения

это хорошее замечание, но я собираюсь эту тему разобрать в визуальном контексте, там будет очень хорошо видно на что оно влияет, мне кажется. Нельзя же все засунуть в одну статью.

а не шлепать непонятные примеры

мне кажется непонятные примеры интереснее всего разбирать.

Хватит. То что вы перевели статью от Тауба про async/await не делает вас экспертом в этой теме. Хватить писать эту псевдонаучную ересь.

Ахах, черт возьми, у статьи еще и тег "туториал". )

неужели даже такие физиономии получают приглашение от @habrahabr ?

На личность перешли? А еще ниже можете?

На личность перешли? А еще ниже можете?

Так это был старт соревнования в низости! Не знал!

Тогда вы сразу победили с этой личностью, которую вы тут вывалили.

Какое соревнование? Вы о чем вообще? Этот коментарий такой же бессмысленный как и предыдущий. Как и ваши последние статьи, к слову.

Б-же, какая каша у автора в голове... Я не шарпист, я сишник, и автору тоже рекомендую начать с азов - со Стивенса и select() на Си, чтобы хотя бы начать понимать разницу между неблокирующимися вызовами и асинхронными. Потом треды и конечные автоматы. Когда устройство на низком уровне будет усвоено, станет понятно, как работает реализация на высокоуровневом языке. И весь этот псевдофилософский сумбур ("объект действия"? интуитивно понятен? ЩИТО?), местами даж фаллософский, станет не нужен.

Интересно, то что создано Чужими для Хищников для вас тоже каша?

Зарегистрируйтесь на Хабре, чтобы оставить комментарий

Публикации

Истории