Pull to refresh

Comments 56

Но может быть я где-то, как-то вас тоже обманываю?

Да. Вы подменяете понятия "поток, выполняющий операцию сейчас" и "поток, на котором операция продолжится после await".

Фраза "There is no thread" совершенно верна в том контексте, в котором она была произнесена. Её смысл в том, что нет потока, который сидит и ждёт, пока завершится ваш ReadFileAsync().
Она никогда не означала "после завершения await код продолжит выполняться на том же потоке, что до начала await". На каком потоке продолжит выполняться код, зависит от контекста синхронизации.

Да. Вы подменяете понятия "поток, выполняющий операцию сейчас" и "поток, на котором операция продолжится после await".

интересно, как вы определяете, когда у нас случается "сейчас"?

"Сейчас" - в контексте критикуемой вами статьи и её главной мысли - это пока выполняется ваш await, который вы уже вызвали и который уже ушёл из вашего кода, через фреймворк, через драйвер в железо, и пока не вернулся. Утверждение "There is no thread" - это про то, что в этом "сейчас" нет никакого потока, который занимается ожиданием возвращения вашего await.

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

А вы пытаетесь это утверждение опровергнуть, приводя примеры про "после", когда вышеупомянутый await наконец вернулся из железа, через драйвер, через фреймворк в ваш код, и следующий за ним код вдруг выполняется в другом потоке

из какого нафик железа, вот так вот я имею право написать:

static async Task SomeMethodAsync555()
  {
  //start logic 
  await SomeMethodAsync1();
//end logic 
  }

Та же самая функция из моего примера только под await-ом как раз там где у вас "сейчас", поток в наличии под await-ом. Как жить дальше?

из какого нафик железа

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

Прочтите хотя бы до середины, до слов

The write operation is now “in flight”. How many threads are processing it?
None.
There is no device driver thread, OS thread, BCL thread, or thread pool thread that is processing that write operation. There is no thread.

При этом ваши выводы - о том, что код продолжил выполняться на другом потоке, чем до этого - совершенно ортогональны статье и никак не опровергают и не подтверждают её.

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

Прочтите хотя бы до середины, до слов

А вы прочитали то, что я написал? Вы отвечаете на аргументы, которых я вам не давал.

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

Дело не в самом железе, а в точке достижения незавершенного await (в случае с железом это будет как раз точка достижения железа).

Каждый await вниз по цепочке вызовов выполняется синхронно, до первого await, который не сможет выполниться синхронно и задействует вместо этого конечный автомат. В этот момент вызов вернется в начало цепочки, и поток, который этот вызов начал (и у которого теперь на руках незавершенный await) окажется свободен. Он вернётся туда, откуда пришёл - в случае с консольным приложением в тредпул, в случае с winforms - в петлю сообщений. И будет там заниматься своими делами, а не сидеть и ждать, пока вернётся его незавершённый await. Поэтому можно говорить, что there is no thread, которая сидит и ждёт.

Этот принцип вы никак не опровергаете своими примерами.

кстати, вот чуть ниже @AlexDevFx написал вопрос:

где это явным образом написано, что "асинхронная операция НИКОГДА" не создает поток? Есть ссылки?

я правильно понимаю, вы как раз считаете что "асинхронная операция НИКОГДА не создает поток"? Хотя бы асинхронная операция вызванная под await-ом.

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

Асинхронная операция не создаёт поток в том смысле, что его создаёт не она. Поток создаёт (или не создаёт) контекст синхронизации.

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

Я же пытался обратить внимание что асинхронная операция все таки МОЖЕТ (в принципе) использовать дополнительный поток, то есть что поток может существовать, и значит не правильно говорить что его НИКОГДА нет. Вопрос не в том кто его создает, а в том может ли он быть под асинхронной операцией.

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

Короче в этой заметке вы опровергли свое неверное понимание оригинальной статьи. Ни больше ни меньше.

А еще вы путаете понятия "асинхронная операция" с "асинхронным методом". Вообще не одно и то же.

Короче в этой заметке вы опровергли свое неверное понимание оригинальной статьи.

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

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

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

Асинхронный метод - это тот что помечен как async. Асинхронная операция - это грубо говоря I/O. Например чтение файла с диска (вот вам железо) или сетевой запрос (и это тоже железо). Эти операции выполняются какими-то там драйверами или еще чем, я не разбираюсь. Но точно знаю, что пока они выполняются, в .NET'овском коде не нужен поток который будет ждать их завершения. То есть, по сравнению с синхронными методами, выполняющими асинхронные операции, нужно гораздо меньше потоков что бы обслуживать большее число запросов. Потому что пока асинхронная операция не завершилась, ее поток может быть использован другим запросом. И об этом статья. Ваш же код, который как вы утверждаете, ее опровергает, совсем не о том. Так что выпад "я художник, я так вижу" - не уместен. Особенно если вы начали с высокомерного заявления намекая на то, что вы то во всем разобрались.

Логика тех, кто поддается такому внушению мне вполне понятна, они хотят упростить себе жизнь, сократить объем теории, с которой надо разбираться.

Интересно было бы понять логику тех, кто поддерживает такое внушение, выдавая обрезанную теорию за полноценную, вполне осознавая, что все не так просто, как хотелось бы.

Вот двое ваших предшественников так же рванули писать статью на Хабре, о том как круто они шарят и всех ща научат (спойлер: не научили)

Разобраться раз и навсегда: Task.WhenAll или Parallel.ForEachAsync в C#

Как легко получить deadlock на Task.WhenAll

А разве CPU-bound операция не может быть асинхронной операцией?

Не может. Она может быть параллельной, но не асинхронной.

Майкрософт, кажется, с Вами не согласен:

You could also have CPU-bound code, such as performing an expensive calculation, which is also a good scenario for writing async code.

Asynchronous programming scenarios

Код будет выполняться асинхронно в том смысле, что поток запустивший CPU-bound оерацию не будет ожидать ее завершения. Но эта операция не будет ассинхронной, она будет параллельной. Потому что для нее будет запущен отдельный поток, который будет работать в то время, как запустивший ее поток будет делать что-то другое. Понятия асинхронности и параллельности часто путают, потому что они не совсем ортогональны и если не впадать в формализм, бывают взаимозаменяемыми.

Но эта операция не будет ассинхронной, она будет параллельной.

Ну, почему-же? Если это будет чисто вычислительный метод, но - помеченный как async и не забывающий время от времени делать await Task.Yield(), то он вполне может работать и в однопоточном контексте синхронизации - по очереди с основным методом, получая от него управление когда он, в свою очередь, делает await. То есть, выполнение получается квази-параллельным. Лучше всего оно описывается старым, (но ныне ставшим популярным у плюсовиков, потому что недавно в стандарт C++ наконец-то завезли их поддержку) словом "сопрограмма".

Собственно в системах с коооперативной многозадачностью - Win3.x, к примеру - мы так и жили тогда: там был ровно один поток управления. Только тогда не было такого замечательного компилятора, который был способен развернуть async/await в машину состояний, и приходилось отдачу управления делать по-другому: например, через вторичный цикл сообщений. Или - явно разбивать выполнение на куски и запускать их по очереди через PostMessage.

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

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

Вы тут описали лабораторные условия, не имеющие никакого отношения к реальному коду в современном C#.

То есть, выполнение получается квази-параллельным

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

Автор же этой статьи исказил оригинальный вопрос, а потом опроверг ту статью дав ответ на свой искаженный вопрос. Он назвал это "субъективное мнение", и еще что-то там. Но как по мне, он просто не может признать что не верно понял вопрос, и продолжает зарывать себя еще глубже нелепыми отмазками.

The objectors to this truth are legion. “No,” they cry, “if I am awaiting an operation, there must be a thread that is doing the wait! It’s probably a thread pool thread. Or an OS thread! Or something with a device driver…”

Heed not those cries. If the async operation is pure, then there is no thread.

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

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

вы не можете с await-ом вызвать какую-то синхронный метод:

HttpRequest(...)

должна существовать асинхронная версия

HttpRequestAsync(...)

того же метода, которую кто-то все таки написал.

Вы опять делаете вброс не имеющий отношения к теме.

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

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

(*) я пометил это место звездочкой чтобы сразу отмести нелепые доводы, типа, если там будет `await Task.Run(...)`, или будет async метод, но без await и тому подобное. Речь не об этом.

Статья Стивена Клери не появилась на пустом месте. В 2012 году C# показал людям что можно писать код, который выглядит как синхронный, но по факту имеет преимущества асинхронного. И людям было сложно понять эту магию. Все таки много лет они мыслили синхронными методами. Давайте возьмем простой пример, что бы понять в чем была сложность и почему появилась та статься.

Вот вам код:

public void DoSomeWork()
{
    Console.WriteLine("Getting string...");
    var str = GetString();
    Console.WriteLine($"Received string {str}");
}

public string GetString()
{
    var request = CreateRequest();
    var response = ExecuteRequest(request);
    var responseString = response.GetString();
    return responseString;
}

Программист мыслил так:

  • Сначала мой код выведет `Getting string...`

  • Потом создаст запрос и отправит его на выполнение

  • Тут я буду ждать ответа, и только когда он вернется, я вытащу из него строку и выведу результат на экран

  • Это не эфективно, но зато прямоленейно, легко пишется и читается.

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

var responseString = response.GetString();
return responseString;

Console.WriteLine($"Received string {str}");
  • Но это сломает программу поэтому мне нужно написать ее иначе. Нужно перекинуть продолжение в асинхронный колбэк. Если продолжение метода GetString это не сложно, то вот с DoSomeWork уже сложнее.

  • А что если перед DoSomeWork есть еще длиннющая цепочка вызовов?

  • Ну его нафиг, я просто использую синхронный вызов.

И тут приходит C# 5 и говорит: не надо менять структуру кода, просто добавть пару волшебных слов и все станет ассинхонным.

И программист такой: но как же так? Ведь response.GetString(); будет выполнен до окончания ExecuteRequest. Это сломает мой код!

C#: нет нет, твой код не продолжит выполнение. Он будет как бы заблокирован, но не заблокирован. Вот такая вот магия.

Программист: мне кажется ты лжешь. Ты запустишь где-то поток, который будет ждать ответа, а потом как-то вернешь управлнеие мне что бы я продолжил с того же места где закончил. Я не вижу колбэков! Кто-то должен дождаться ответа!

И вот это было самым сложным для понимания. Код выглядил линейным но выполнялся асинхронно. Все задавали этот вопрос, и Стивен прекрасно справился с ответом.

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

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

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

Неужели вам нужно все разжевывать? Неужели не понятно что речь идет о том, что есть асинхронная версия с BeginInvoke? Мы в 2012, помните?Вместо того чтобы сосредотояится на главном, вы вылавливаете всякие закавырки не сказаные напрямую и придераетесь к ним, уходя от основной темы разговора. Просто поразительно насколько плохо у вас с абстрактным мышлением.

Неужели вам нужно все разжевывать? Неужели не понятно что речь идет о том, что есть асинхронная версия с BeginInvoke? Мы в 2012, помните?

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

Вы тут описали лабораторные условия, не имеющие никакого отношения к реальному коду в современном C#.

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

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

Лично я привык вообще думать не в этих новомодных терминах, а -в понятиях "задача", "поток выполнения(который по-анлийски - flow, а не thread), "многозадачность" - "кооперативная" и "вытесняющая" - "исполнитель" (обычно - "исполнительное устройство", но может быть и человек), "параллельность" работы разных исполнителей и прочим из старых книжек, по которым я учился: проблема неэффективности чисто поледовательного выполнения, если что, была осознана в индустрии еще лет шестьдесят назад, если не больше. Я знаю, как эти понятия определить для себя и, если надо - сформулировать для других, чтобы определиться, о чем речь.

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

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

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

Более того, современный CPU в реальности состоит из некоторого количества более или менее специализированных исполнительных устройств, работающих более-менее параллельно, так что для более полной их загрузки может иметь смысл спланировать выполнение на них двух квазинезависимых потоков: Intel, к примеру, использует эту технологию, называя ее hyper-threading. А потому в ряде применений вполне может быть осмысленно разделить операции, даже упирающиеся в один CPU, но требующие, в основном, разных исполнительных устройств этого CPU, на две задачи для двух потоков, выполняющихся, пусть и на одном ядре, но в значительной степени параллельно. Например, если программа считает какой-нибудь условный матан и результаты выводит в какой-нибудь условный джейсон - то есть, для этого сравнивает и перемещает байтики, складывает-вычитает чисто целые числа и делает прочие простые операции. то такая программа вполне может почти что параллельно на одном и том же ядре CPU в одном потоке считать матан, а в другом - делать из него джейсон, тем самым более полно загружая все исполнительные устройства ядра.

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

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

Вот это здорово, впервые вижу эти определения сформулированные таким образом: просто и понятно.

Это где это явным образом написано, что "асинхронная операция НИКОГДА" не создает поток? Есть ссылки?

Я же написал (имел ввиду, явно не написал), что мне кажется(!) что нам хотят это внушить, при чем здесь ссылки?

Как вы себе представляете ссылку на то, что мне показалось?

Спасибо, что подтвердили моё предположение, что проблема надуманная. В принципе, никто в статьях ничего не внушает, а просто описывают как работают асинхронные вызовы под капотом.

ну хорошо я процитирую, из статьи которая называется "нет потока":

We already know that the UI thread is not blocked during the await. Question: Is there another thread that must sacrifice itself on the Altar of Blocking so that the UI thread may live?

то есть речь в статье идет как бы о том что

There Is No Thread that is blocked while UI thread is not blocked during the await.

Это конечно очень субъективно, но мне кажется это не честный прием заменить-сократить фразу:

"Нет потока, который блокируется во время await"

на/до фразы:

"Нет потока".

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

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

Вашу статью можно уложить в одну фразу: "Заголовок не соответствует содержанию поста", или "Заголовок не отражает сути статьи", или "Автор использует кликбейтный заголовок". Вот и всё. Не надо приписывать автору намерение запутать всех, которого там нет.
И как по-вашему внушение происходит? Начинаюший программист ищет информацию в поисковике по асинхронным методам, видит статью "There is no thread", не читая её, решает: "Ага, значит асинхронный воркфлоу не создает поток. Мне всё понятно теперь."
И всё бы неплохо было бы с точки зрения: "Я имею право на субъективное мнение", если бы мы обсуждали картину Босха. Но тут идёт речь о конкретных технических вещах, где уже раз 100 всё разложили. Какой может быть субъективизм на счёт асинхронного алгоритма?

Начинаюший программист ищет информацию в поисковике по асинхронным методам, видит статью "There is no thread", не читая её, решает: "Ага, значит асинхронный воркфлоу не создает поток. Мне всё понятно теперь."

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

Именно в том что те, кто цитирует это название, считают что оно отвечает на все вопросы, именно в этом я вижу проблему.

Почитайте The Leprechauns of Software Engineering о том как рождаются легенды, тут примерно также - потоков нет, всю работу в асинхронных операциях делают Лепреконы, но я с этим почему-то не согласен. И вам, похоже, это очень не нравится. Очень интересно почему. Может быть и вы боитесь потерять универсальный аргумент?

Извините, но как можно стать продолжающим программистом, читая только заголовки? Да, можно заблуждаться, но рано или поздно человек столкнётся с проблемой. И он либо разберётся - всё же прочтёт статью, либо перестанет быть программистом. В любой профессии есть дилетанты, но их процент очень мал, так как они либо развиваются, либо уходят из профессии. Остаться можно за счёт коррупции. Но много ли таких, чтобы поддерживать существование легенды?
Мне не нравится что качество материала на Хабре катится вниз, когда выходят расследования в духе Михалкова: "Вот смотрите я нашёл патент. Билл Гейтс хочет поработить людей".

Тут же в цитате написано, что "Мы уже знаем, что UI поток не блокируется при await."

Каким раком Вы из этого сделали вывод, что There Is No Thread?

Это в корне неправильный вывод из предлжения в цитате.

Дальше спорить даже нету смысла, так как ошибка в Вашем вопросе к людям.

Не создает асинхронный код потоки :)

Грубо говоря, асинхронный метод делится на части и превращается в некоторую стейт машину. Можно сказать, что разделение проходит по await'ам. Т е до первого await'а выполнение происходит синхронно, затем стейт-машина эти кусочки (continuation'ы) по очереди шедулит в synchronization context.

В вашем случае, используется дефолтный контекст, который шедулит continuation'ы на потоки из тред-пула. Более того, вы не await'ите первый вызов, т е делаете fire and forget - глупо ожидать, что две параллельные задачи в таком контексте начнут выполняться на одном потоке.

То есть, эти потоки они уже есть в наличии, и их использование дефолтным контекстом вполне оправданно.

Что касается контекстов UI - ну, во-первых, название метода вообще ни о чем не говорит. Во-вторых, добавьте .ConfigureAwait(false) и получите немалый шанс того, что после этого await'а поток поменяется. Не потому, что кто-то создал новый поток. Просто для continuation'а может быть использован другой контекст - тот же дефолтный, на тред пуле.

Не создает асинхронный код потоки :)

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

Когда речь еще не шла об асинхронных операциях, а потоки во всю использовались, код с потоками можно было называть асинхронным, как вы думаете?

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

Код, который создает потоки - это код, который явно делает Thread.Start. Создание потоков, кстати, это относительно ресурсоёмкая операция, да и память отжирает понемногу.

Асинхронный код - это какой-то код внутри асинхронного метода с асинхронными вызовами (await), раскладываемый компилятором на стейт-машину.

Так вот, если вы явно не вызываете Thread.Start в своем асинхронном коде - он потоков не создает. Контекст может создавать новые потоки или переиспользовать в ходе выполнения асинхронного метода. Но контекст != асинхронный код. И то, что в каких-то случаях асинхронный код может использовать такой контекст, совершенно не обозначает, что асинхронный код создает потоки.

Асинхронный код - это какой-то код внутри асинхронного метода с асинхронными вызовами (await), раскладываемый компилятором на стейт-машину.

Есть такое выражение: "практика, критерий истины". Я готов согласиться с вашим определением (как и с любым другим), когда будет понятно как это определение, именно в таком виде, помогает для /решения/-/понимания способов решения/ задач на практике. Зачем нам надо обращать внимание на то, кто вызывает функцию Thread.Start , на что это влияет.

Определения же даются с какой-то целью, обычно для того чтобы сократить какие-то дальнейшие пояснения.

Поэтому если вы взялись писать определения и вводить термины будьте добры продемонстрировать их применение, а то, что что-то != чему-то, это какой-то странный результат рассуждений, ну не равно, а что дальше то? или:

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

в контексте моей статьи это и не анализировалось, и я пока не вижу, опять же, что, на практике, зависит от того, считаем мы, что асинхронный код может создавать потоки, или выносим эту функцию на какой-то другой уровень, чем отличается аренда потока от создания. Вы тогда этот уровень как-то должны определить-идентифицировать, а то получается, что вы просто хотите проигнорировать эту функциональность, типа: вот тут у меня асинхронный код, а все остальное меня не интересует. Я против такого подхода конечно, но я надеюсь это не про вас.

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

Зачем нам надо обращать внимание на то, кто вызывает функцию Thread.Start

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

ну не равно, а что дальше то

Божественная аргументация :) Не думал, что это потребует дополнительных объяснений. Вот, например, есть у вас автомобиль, и вы его используете. У него есть двигатель. Верно ли будет сказать, что вы != двигатель, или вы несогласны? Вот, например, есть ДВС, а есть электродвигатель. ДВС загрязняет окружающую среду. Правда ли то, что если у вас есть автомобиль, вы загрязняете окружающую среду? А если там электродвигатель? А если он просто в гараже стоит?

Я честно говоря думал, что мысль крайне простая и доступная.

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

Серьёзно? Вы не видите разницы между арендой потока и созданием? :)

получается, что вы просто хотите проигнорировать эту функциональность, типа: вот тут у меня асинхронный код, а все остальное меня не интересует

Это не совсем так работает. Это называется абстракция и декомпозиция - вот у нас есть контекст синхронизации (это абстракция), он будет отвечать за детали логики выполнения continuation'ов. И да, с точки зрения асинхронного кода то, как это будет происходить - я могу проигнорировать, так же, как это осознанно сделали разработчики .NET. Это значительно упрощает разработку.

Верно ли будет сказать, что вы != двигатель, или вы несогласны?

начать то надо с того, а кто же вам будет задавать такой вопрос и с какой целью. Вы не находите, что вопрос достоин клиента психбольницы? Неужели вы серьезно задаетесь такими вопросами или отвечаете на них?

Я надеюсь это шутка такая была.

Я вот тоже думал, что это шутка такая, а вы по сути что-то такое и утверждаете

Очередной разрушитель легенд, который не понимает разрушаемую легенду 🤦. И достаточно грубо отвечает тем, кто совершенно корректно объясниет где его ошибка 🤦🤦🤦

Для начала

  1. Что такое асинхронное выполнение и какими инструментами его можно добиться

  2. Что такое континуации/фьючи

  3. причем тут коллбеки

  4. что такое контекст выполнения асинхронных функций и методов в языках с поддержкой тредов

  5. чем отличаются точные науки (например программирование) от религиозной горячки

@rukhi7 спасибо за ответ в статье. Почитав комменты я понял, что большинство споров вызвано непонимание контекста вопроса, на которая отвечает данная статья. Создаются ли потоки неявно при использовании async/await? Ответ да, если не использован контекст синхронизации, который выводит выполнение в один поток (при этом не используется ConfigureaAwait(false) и Task.Run()). Вопрос не касается О и на скольких потоках некоторые операции выполняются на низком уровне.

А в чем заключается упомянутая в статье сложность прямого использования потоков? Потому как гением я не являюсь, и значит раз я этой сложности не вижу, значит я что-то упускаю.

раз я этой сложности не вижу, значит я что-то упускаю

Вообще есть, по моему, целых три варианта, почему можно не видеть этой сложности:

  1. как вы сказали: я что-то упускаю

  2. ваши задачи действительно не дошли до достаточного уровня сложности

  3. вам все пофигу :)

  4. а! еще один, это ваша любимая работа, она доставляет вам удовольствие - сложность не является проблемой, у меня где-то примерно так, сложность еще надо идентифицировать в каждом конкретном случае.

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

Утверждение в древней статье: If the async operation is pure, then there is no thread.

Вы: я написал async await - и получил дополнительный поток.

Возможные варианты:

  1. Автор статьи всех обманывает уже 10 лет (ваш вывод)

  2. Ваш пример - это не pure async operation в том смысле, который вкладывал в него автор статьи.

async / await скопировали себе многие языки. Включая javascript, в котором поток всего один. Сама идея async / await - синтаксически удобный способ избегать блокировки потока при IO-bound вызовах. Понятно, что можно держать его неправильно и вручную явно/неявно запустить другой поток. И что там внутри в реализации на самом деле I/O completion threads используются даже при pure операциях. Не используйте async/await неправильно, всего делов :)

Не используйте async/await неправильно, всего делов :)

у вас, вроде как получается, что только pure async operation являются правильным использованием async/await, или какой смысл вы в это вкладываете?

Смысл - если ваша цель - экономия потоков, то правильным использованием будет  pure async / await. Потому что при неправильном использовании экономия потоков исчезает.

Но вообще я о том, что вы сами придумали утверждение, которое автор оригинальной статьи не делал, и сами же его опровергли:

асинхронная операция НИКОГДА не создает потока и в принципе не может его создать, но это не правда

vs

 If the async operation is pure, then there is no thread

Есть очень много утверждений, которые станут ложными при замене If [condition] -> НИКОГДА :)

Смысл - если ваша цель - экономия потоков, то правильным использованием будет  pure async / await. Потому что при неправильном использовании экономия потоков исчезает.

опять же, у вас получается, что async / await нельзя использовать для CPU-bound work которая требует background thread. Когда поток действительно нужен для параллельной работы его нельзя сэкономить.

Но вообще я о том, что вы сами придумали утверждение

ну мне кажется утверждение There is no thread вполне можно интерпретировать как

асинхронная операция НИКОГДА не создает потока и в принципе не может его создать

Да это мое ИМХО. Проблема в том что эту фразу, а это заголовок, обычно приводят как аргумент именно с таким смыслом как я написал, мне кажется. Проблема в том что даже если автор и не вкладывал в эту фразу такой смысл, судя по содержанию его работы, эта фраза-этот заголовок подталкивает именно к такому смыслу. Что плохого в том, что я обратил на это внимание?

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

Полностью согласен :)

Что плохого в том, что я обратил на это внимание?

Плохого - ничего. Проблемного - у новичков проблема с пониманием разницы между распараллеливанием и асинхроностью. Чтобы ее побороть - им пытаются объяснить что это две (почти) ортогональные вещи, что async/await не запускает потоки (сам по себе), и что это все про экономию, а не про "обработку в фоне".

А статья в стиле "вас обманывают, смотрите, там запускаются потоки!" исправлять эту проблему не помогает. Вот что у новичка, не понимающего как работает scheduler и task.yield отложится в голове? "видел статью на хабре, там на примере в 10 строк написано, что async/await на самом деле запускает фоновый поток и в нем все крутит". Вы же не раскрыли почему поток запускается, и как этого избежать, не расписали что "это потому что не pure, не делайте так, дети". Сделали небольшой трюк в коде, который джун не заметит и не поймет - и натянули на результат сенсационные опровержения "вам врут". Пользы от этого - никакой, и немного вреда. А не-новички и так понимают механизм.

Чтобы ее побороть - им пытаются объяснить что это две (почти) ортогональные вещи

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

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

Спасибо за содержательные ответы.

Так все верно - асинхронный код не создает поток.
Поток создает task, он же создает асинхронный объект, хранящий код после awake и элементы синхронизации для того, чтобы асинхронный код получил управление и данные из другого потока в своем.
Асинхронный код работает с
var obj = await SomeAsyncObject();
То, что последний создает поток - не его проблема

Sign up to leave a comment.

Articles