Когда нам показывают на некотором примере, что асинхронная операция не создает потока, нам пытаются внушить, что асинхронная операция НИКОГДА не создает потока и в принципе не может его создать, но это не правда! Простой пример с работающим кодом доказывает обратное. Давайте разберем этот пример.

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

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


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

        static async Task Main()
        {
            SomeMethodAsync1();
            Console.WriteLine($"ThrID={Thread.CurrentThread.ManagedThreadId}");
            for (int i = 0; i < 5; i++)
            {
                Console.WriteLine($"MAIN Iter={i}");
                Thread.Sleep(100);
            }
        }

        static async Task SomeMethodAsync1()
        {
            await Task.Yield();
            Console.WriteLine($"TrID={Thread.CurrentThread.ManagedThreadId}");
            for (int i = 0; i < 5; i++)
            {
                Console.WriteLine($"MethodIter={i}");
                Thread.Sleep(100);
            }
        }

Этот код генерирует следующий вывод:

Собственно, уже из этого вывода видно, что две функции выполняются в разных потоках с идентификаторами: ID=1 и ID=5.

 Можно рассмотреть эти потоки N1 и N5 более пристально в режиме отладки:

Not Flagged                12268  1          Main Thread   Main Thread            System.Private.CoreLib.dll!System.Threading.Thread.Sleep

Not Flagged    >           26956  5          Worker Thread           .NET ThreadPool Worker            PrimMath.dll!PrimMath.Program.SomeMethodAsync1

Мы видим, что потоки у нас берутся из Тред-пула, то есть, формально говоря, наша функция SomeMethodAsync1() не создает поток, она берет существующий из Тред-пула. Но никак нельзя отрицать тот факт, что асинхронная операция все-таки использует дополнительный поток, то есть утверждение There is no thread оказывается ложным для этого кода, потому что мы явно видим, что The thread with ID5 is definitely present.

Но может быть я где-то, как-то вас тоже обманываю? Если вы не верите мне и моему примеру и своим глазам, вы все также можете обратиться к незабвенному Stephen Toub и к его уже изрядно всем надоевшей (надеюсь это не так :) работе How Async/Await Really Works in C#. Параграф:

SynchronizationContext and ConfigureAwait

Там есть пример консольного кода, который далее в том Посте упоминается как our timing sample и который тоже создает дополнительный поток для async lambda функции. Собственно то, что асинхронный метод создает дополнительный поток, и является проблемой, которую автор Поста далее успешно решает, когда подменяет SynchronizationContext для этого исходного примера. Собственно, из того примера Стивена Тоуба вы и должны были сделать вывод, что наличие или отсутствие потока внутри асинхронного метода управляется как раз с помощью SynchronizationContext, который, в некотором смысле, можно считать дополнительным планировщиком (scheduler), который предоставлен нам средой исполнения .NET, и который вы можете переопределить под свои задачи в любой момент.

А вот теперь вооружившись этим знанием о том, что наличием или отсутствием потока
для асинхронных операций управляет контекст синхронизации (SynchronizationContext),
давайте взглянем на пример, который разбирают в статье There is no thread. (Там нет потока):

private async void Button_Click(object sender, RoutedEventArgs e)
{
  byte[] data = ...
  await myDevice.WriteAsync(data, 0, data.Length);
}

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

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

 Ну вот, надеюсь я ответил на вопрос от @Grave18

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

Еще раз благодарю за конструктивный вопрос.

Комментарии (31)


  1. slepmog
    15.03.2024 11:19
    +11

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

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

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


    1. rukhi7 Автор
      15.03.2024 11:19

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

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


      1. slepmog
        15.03.2024 11:19
        +1

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

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


        1. rukhi7 Автор
          15.03.2024 11:19

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

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

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

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


          1. slepmog
            15.03.2024 11:19
            +4

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

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

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

            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-а.


            1. rukhi7 Автор
              15.03.2024 11:19

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

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

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


              1. slepmog
                15.03.2024 11:19
                +1

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

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

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


            1. rukhi7 Автор
              15.03.2024 11:19

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

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

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

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


              1. slepmog
                15.03.2024 11:19

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


                1. rukhi7 Автор
                  15.03.2024 11:19

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

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


              1. cstrike
                15.03.2024 11:19
                +1

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

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

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


                1. rukhi7 Автор
                  15.03.2024 11:19

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

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

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

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


                  1. cstrike
                    15.03.2024 11:19

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

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

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

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

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

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


                    1. vadref
                      15.03.2024 11:19

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


                      1. cstrike
                        15.03.2024 11:19

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


                      1. vadref
                        15.03.2024 11:19
                        +1

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

                        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


                      1. cstrike
                        15.03.2024 11:19

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


  1. AlexDevFx
    15.03.2024 11:19
    +2

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


    1. rukhi7 Автор
      15.03.2024 11:19

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

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


      1. AlexDevFx
        15.03.2024 11:19

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


        1. rukhi7 Автор
          15.03.2024 11:19

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

          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"

          на/до фразы:

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

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

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


          1. AlexDevFx
            15.03.2024 11:19
            +1

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


            1. rukhi7 Автор
              15.03.2024 11:19

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

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

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

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


              1. AlexDevFx
                15.03.2024 11:19

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


          1. Kingas
            15.03.2024 11:19

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

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

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

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


  1. aftertherainbow
    15.03.2024 11:19

    ..


  1. gdt
    15.03.2024 11:19

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

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

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

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

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


    1. rukhi7 Автор
      15.03.2024 11:19

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

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

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