Концепция Message Passing. Агенты и актёры

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

    Message passing является одной из популярных концепций параллельного программирования. Она часто используется при создании сложных распределенных систем с высокой степенью параллелизма. Реализация этой концепции представлена в языках программирования в качестве актёров (actor) или агентов (agent).


    Расспределенные агенты HostTracker. Быстрая проверка с http://updownchecker.com


    Особенности message passing:

    • Решение состоит из изолированных компонент, которые работают параллельно (в параллельных потоках из пула потоков). Взаимодействие между компонентами идет через обмен сообщениями по определенному протоколу. Сетевые компоненты могут использовать TCP, UDP, HTTP, и т.д. Локальные взаимодействуют через протокол, определенный конкретный языком его реализации или библиотекой.
    • Компонент определяет логику обработки входных сообщений. Последние попадают в очередь (queue) и последовательно достаются из нее для обработки.
    • Компонент может быть владельцем некоторых ресурсов и быть их провайдером для других компонент. Ресурсом могут быть: данные в определенном формате в оперативной памяти, аппаратно-программный ресурс или их комбинация.
    • Компонент имеет определенное состояние, которое может инкапсулировать ресурс (из предыдущего пункта), или, как в случае машины состояний (state machine), может быть выражен в виде определенного алгоритма обработки сообщений, который переводит его в другое состояние.
    • Интерфейс между компонентами:
    — postSync, postAsync – послать сообщение компоненте синхронно или асинхронно.
    — receiveSync, receiveAsync – получить сообщение от компонента синхронно или асинхронно. Асинхронное ожидание состоит в том, что поток как ресурс возвращается системе и может быть использован для другой работы. В этом случае система регистрирует функцию обратного вызова (callback) на определенное событие.
    — tryReceive функции – аналогичные вышеперечисленным, но имеющим определенную задержку для получения данных.
    • Концепция тесно связана с понятием асинхронного исполнения.

    Асинхронное и параллельное исполнение

    Асинхронное исполнение значит, что поток не ожидает результат исполнения определенной операции, используя при этом ресурсы операционной системы. Вместо этого, поток возвращается в пул и может быть использован для других операций. В ОС при этом регистрируется callback-функция, которая принимает результат асинхронной операции. Эта функция запускается по факту получения результата (в ОС Windows через механизм портов завершения IO Completion ports, в Nix системах через другие механизмы ядра).

    Реализации концепции message passing

    Реализация Язык Тип взаимодействия Особенности Разработчик
    Erlang processes Erlang локальный, сетевой Первая реализация для телекоммуникационного оборудования. Может использоваться для сетевого взаимодействия. Определен метод отсылки сообщений вида Pid! message и receive с паттерн матчингом сообщения. Ericson corp.
    MailboxProcessor F# локальный Использует асинхронные вычисления. Дополнительно появляется понятие канала ответа – AsyncReplyChannel, который позволяет получат ответ синхронно и асинхронно. Microsoft research
    TPL dataflow .NET языки локальный Набор примитивов – блоков, которые реализуют приемники и источники данных. Microsoft
    Scala actors Scala (Java machine) локальный, сетевой Реализация напоминает классическую – Erlang processes. EPFL, Typesafe Inc.
    NodeJs JavaScript сетевой Event-driven подход к разработке приложения: декларативное определение асинхронных операций и коллбеков, очередь вызова обработчика событий. Внешний интерфейс определяется разработчиком приложения. Joyent Inc.
    MPI:
    MPICH, HP-MPI, SGI-MPI, WMPI,
    C, C++, Java, Fortran сетевой Специфицированный интерфейс взаимодействия узлов сети. Единый исполняемый код для всех узлов с выбором ветки исполнения по идентификатору процесса. Microsoft, HP, SGI, Argonne National Laboratory, others


    Разные языки программирования по-разному поддерживают эту концепцию. Так, JavaScript (платформа NodeJs) является примером Event-Driven языка (язык, который имеет встроенную поддержку работы с асинхронными вычислениями). Типичная программа на NodeJs – декларативная регистрация callback функций на ответы событий IO. В начале ее исполнения создается очередь операций, в которой и срабатывают callback-функции. То есть сама программа является однопоточной, но асинхронной. То есть сама программа является однопотоковой, но асинхронной. В данном случае виртуальная машина NodeJs (основана на Chrome v8 JavaScript runtime) является примером реализации message passing и удовлетворяет все вышеперечисленные условия, если реализует определенный сетевой интерфейс взаимодействия с другими компонентами распределенного применения (например, REST сервис).

    В некоторых языках (например, F# — async монада, С# — async, await) существует понятие асинхронного вычисления – части кода, определенной разработчиком, которая содержит как синхронные, так и асинхронные операции. Также, существует возможность запустить вычисления в параллельном потоке, отменить вычисления, сделать синхронный запуск.

    Примеры реализации концепции Message Passing

    Концепция message passing впервые была представлена в языке Erlang (функциональный язык с динамической типизацией), который был разработан компанией Ericson для специализированного телекоммуникационного оборудования. Процессы Erlang представляют собой компоненты, описанные ранее. Их создание и обмен данными между ними, в отличии от процессов ОС, не нуждается значительного временного ресурса. Современные функциональные языки, такие как Scala (Java платформа) и F# (.NET платформа) взяли реализацию message passing именно из Erlang. Scala-агенты могут существовать как в рамках одного процесса ОС, так и в разных процессах на разных компьютерах. Принципы работы с ними аналогичны принципам Erlang-процессов. F# предлагает несколько другой подход – MailboxProcessor. Также, свои особенности реализации message passing можно найти в библиотеке TPL Dataflow, разработанной для языков .NETTPL Dataflow — .NET (может быть использована в языках C#, VB, F#);

    Сетевое взаимодействие построено на принципах message passing. Здесь компонентом является программа на отдельной машине, ресурсами являются все аппаратные составляющие компьютера + отдельные функции, которые делегированы данному вычислительному узлу. Данные хранятся в памяти процесса приложения. Интерфейс взаимодействия с другими компонентами (узлами) определяется конкретной технологией, обычно созданной на Berkley sockets. Как пример такой технологии, можно привести MPI (Message passing interface). Его особенность состоит в том, что каждый узел обрабатывает один и тот же код, выбирая отдельную ветку для обработки с помощью условных операторов с использованием идентификатора потока. Нулевой поток является выделенным. Обмен данными по сети происходит через интерфейс, аналогичный вышеупомянутому, (концепция message passing) и использует идентификаторы потока для обозначения источника и/или приемника дынных. Другой пример – сервисы REST и SOAP, которые дают возможность разработчику самому определить сетевой интерфейс. И хотя, на первый взгляд, кажется, что таким образом определенный интерфейс не впишется в концепцию message passing, он является еквивалентом набору операций post (send) и receive. Так, в SOAP вызов сетевой функции происходит обращением по HTTP xml конверта (envelope), который включает информацию про функцию. Аналогично работают функции в REST – используются HTTP запросы с форматом данных, определяемым разработчиком.

    В дальнейших публикациях будет детально рассмотрен MailboxProcessor F# (активно используется в системе HostTracker)и особенности работы с библиотекой TPL DataFlow.

    ХостТрекер
    69,00
    Сервис мониторинга доступности сайтов
    Поделиться публикацией

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

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

      Например, для Эрланга — это OTP.
      Даже для обычной почты, вырастают MS Exchange, IBM Lotus (если не ошибаюсь).

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

        Да-да-да… И они посидели ещё немного. Потом ещё немного. А потом ещё немного. И ещё немного… пока, увы, их это не достало окончательно :-)
          0
          оказывается на достаточно низком уровне
          это не мешает даным примитивам составлять более серьезные библиотеки.
          В часности OTP построена на Erlang-процессах — от них отказа не было.
          На MailboxProcessor-ре достаточно легко реализовать свои агенты как независимые компоненты системы — обернуть их в класс если хотите.

          требуется над этим какая-то обертка, «шаблоны проектирования»
          — в функциональном программировании и с появлением дженериков шаблоны кардинально меняют свой вид (если вы про класику).

          0
          Малость не понятно с какого боку тут «за ансинхронность»?! Так-то понятно, что имея на руках хорошие примитивы send/receive асинхронность получить труда не составит. Но, выносить в «интерфейс» всякие xAsync — это, имхо, перебор.

          Erlang… Определен только метод отсылки сообщений вида Pid! message.

          Я, по-ходу, как-то не так понял о чем речь. Но, вообще-то там ещё замечательный receive «определен». Которого, по большому счету, сильно нехватает многим остальным.

          Концепция message passing впервые была представлена в языке Erlang

          Ну, емнип, OCalm с его концепцией rendez-vous таки постарше будет.

            0
            . Но, вообще-то там ещё замечательный receive «определен»
            — если поток в тредпуле, то этот receive какраз может и заблочить его.
            Если агентов много — это может привести к глобальному деадлоку системы.
            Поэтому есть смысл ожидать асинхронно сообщение — на счет всяких xAsync.
              0
              — если поток в тредпуле, то этот receive какраз может и заблочить его.

              Я дико извиняюсь, но, вы вот это сейчас за Erlang сказали?!

              Если агентов много — это может привести к глобальному деадлоку системы.
              Поэтому какраз есть смысл ожидать асинхронно сообщение — на счет всяких xAsync.

              О как, оказывается…
              А вас не затруднит, поподробней изложить про «ожидать асинхронно»? Просто, хотя бы на пальцах, как вы это себе представляете?
                0
                Я дико извиняюсь, но, вы вот это сейчас за Erlang сказали?!
                — нет, а конкретней про F# и TPL Dataflow. C Erlang-ом глубоко не работал.
                поподробней изложить про «ожидать асинхронно»?
                — поток не лочиться, а отдаеться тредпулу — коллбек через IO complation ports. Возможно в Erlang-е нет такой проблемы и фактически он не нуждаеться в Async методах.
                  0
                  нет, а конкретней про F# и TPL Dataflow. C Erlang-ом глубоко не работал.

                  Еще раз… у вас в статье, в таблице «Реализации концепции message passing» в строче Erlang, есть фраза:
                  Определен только метод отсылки сообщений вида Pid! message.

                  Я взял на себя смелость вас поправить. Т.к. в Erlang «определен» не только «метод отсылки сообщений», но и т.н. receive expression

                  поток не лочиться, а отдаеться тредпулу — коллбек через IO complation ports

                  Перед тем, как продолжить — пара вопросов, если позволите:
                  1. А где тут ожидание-то?
                  2. Я правильно понял, что понятие асинхронности у вас неразрывно связано с многопоточностью?

                  Возможно в Erlang-е нет такой проблемы и фактически он не нуждаеться в Async методах

                  Именно _такой_ проблемы в Erlang нет. Но, при этом асинхронные, скаже так, «методы» очень даже есть :-)
                    0
                    Спасибо за поправку про receive expression.
                    По-поводу ожидания — именно в случае асинхронного ожидания, вычисление, тоесть callback, ожидает события извне (например получения данных с сети или диска). Но сам поток не ожидает этого события. Он отдаеться как свободный пулу для других вычислений.
                    По-поводу асинхронности и многопоточности — вообще разные вещи. Яркий пример — NodeJs — один поток, а все IO асинхронные по умолчанию. Но в F# вы можете создать async монаду, которую либо запустить в паралельном потоке, либо запустить асинхронно ожидать ответа.

                    Именно _такой_ проблемы в Erlang нет. Но, при этом асинхронные, скаже так, «методы» очень даже есть :-)
                    — мне кажется что тут ситуация скорее подобна к NodeJs. Есть одна очередь событий и вы просто регистрируете callback на события. receive expression скорее всего и регистрирует такой callback в Erlang. Но в общем в концепции могут быть как синхронные так и асинхронные методы
                      0
                      Начну, пожалуй, с конца…

                      мне кажется что тут ситуация скорее подобна к NodeJs. Есть одна очередь событий ...

                      Вам кажется.

                      Каждый Erlang «процесс» имеет свой, отдельный, mailbox, heap и даже GC у него свой — личный, т.с. При этом, Erlang VM — в большинстве случаев — это простой однопоточный процесс ОС.

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

                      Вы можете объяснить, что именно вы понимаете под «callback, ожидает»? Как вы вообще это себе представляете? И, я правильно вас понимаю, что в вашем представлении, любой APC — так или иначе — содержит код, который что-то там _ждет_?

                      И ведь верно говорите, что «асинхронность и многопоточность — вообще разные вещи». Но, при этом, все ваши попытки объяснить _асинхронное ожидание_ пока упираются в наличие каких-то пулов и т.п. Как так? Вас самого это не смущает?
                        0
                        Erlang VM — в большинстве случаев — это простой однопоточный процесс ОС
                        — очень подобно к NodeJs. Так что не зря кажется. Erlang процессы легки в том, что виртуальная машина Erlang не создать ОС потока для каждого созданного процесса.

                        APC — функция, выполняющаяся асинхронно в контексте потока. Она розмещается в очереди потока и (вот наконецто это слово) ждет ряда событий (SleepEx, SignalObjectAndWait, MsgWaitForMultipleObjectsEx, WaitForMultipleObjectsEx, or WaitForSingleObjectEx), которые тоже описаны по линке. Там также описан механизм выполнения IO запроса. Пересказывать не буду, почитаете на MSDN.

                        пока упираются в наличие каких-то пулов и т.п. Как так? Вас самого это не смущает?
                        — нет, не смущает. Callback должен выполнится в определенном потоке. Конкретно если брать механиз MailboxProcessor-а в F# или TPL Dataflow, то они выполняються в потоках из пула. Если брать NodeJs или Erlang, то в контексте одного потока.
                          0
                          очень подобно к NodeJs. Так что не зря кажется.

                          Перестанте делать мне смешно, пожалуйста.

                          Вы всерьез сравниваете «обертку» вокруг, емнип, libev… которая без планировщика от слова совсем, с системой, в которой есть полноценный планировщик? Вам самому не смешно?

                          Так я вам ещё вот что скажу: Erlang умеет в SMP, и планировщиков и более одного может быть.

                          Или может я чего-то пропустил, и while(true) перестал вешать node.js? Асинхронный I/O — это хорошо, конечно. Но, не им единым.

                          Erlang процессы легки в том, что виртуальная машина Erlang не создать ОС потока для каждого созданного процесса.

                          Это-то тут причем?! Чесслово, можно подумать, что вы меня пытаетесь убедить в том, что CLR или там JVM тупо мапят свои concurrent entity на потоки ОС один-к-одному. Или что вы до меня этой фразой хотите донести? Что потоки, например, CLR легкие не по той же причине что ли?

                          APC — функция, выполняющаяся асинхронно в контексте потока. Она розмещается в очереди потока и (вот наконецто это слово) ждет ряда событий (SleepEx, SignalObjectAndWait, MsgWaitForMultipleObjectsEx, WaitForMultipleObjectsEx, or WaitForSingleObjectEx), которые тоже описаны по линке. Там также описан механизм выполнения IO запроса. Пересказывать не буду, почитаете на MSDN.

                          Это замечательно, что вы взяли на себя труд почитать немного MSDN на предмет APC. Мне искренне жаль разрушать ваши иллюзии… и, возможно, я вас немного разочарую. Но, APC –!!! та-да!!! – ничего не _ждет_. Как бы вам этого не хотелось.

                          Если уж вам так хочется – то _ждет_ именно _поток_, в очередь которого мы помещаем объект APC. Причем, если вдруг выясняется, что поток таки ничего не ждет, то _планировщик_, при первой же возможности, ткнет его носом в очередь.

                          Собственно, если мне не изменяет мой склероз, все эти «шурушки» (WFSO и т.п.) как раз и реализованы, через «вычитывание» потоком своей APC queue.

                          Так вот… возвращаясь к «асинхронному ожиданию». Сделаете еще одну попытку объяснить-таки что это за зверь такой? Или мне считать, что, по вашему мнению, та же WaitForSingleObjectEx это и есть ф-ция «асинхронного ожидания»?

                          нет, не смущает. Callback должен выполнится в определенном потоке. Конкретно если брать механиз MailboxProcessor-а в F# или TPL Dataflow, то они выполняються в потоках из пула. Если брать NodeJs или Erlang, то в контексте одного потока.

                          Во-первых, предлагаю вам перестать фантазировать на предмет Erlang.
                          Во-вторых, предлагаю взять то, что вам ближе, и показать уже наконец уважаемой аудитории это ваше «асинхронное ожидание» на примере. С подробным разбором для не сведущих: кто, где, чего и как _ждет_, если вас не затруднит. Заранее спасибо.
                            0
                            Вы всерьез сравниваете «обертку» вокруг, емнип, libev… которая без планировщика от слова совсем, с системой, в которой есть полноценный планировщик
                            — всерьез не сравнивал. Просто увидел подобие на NodeJs в вашем же объяснении. Все зависит от уровня деталей на котором вы концентрируетесь. Глубоко копаете. Поскольку с Erlang-ом опыт мал, то по вашему совету, замолкаю на эту тему.

                            Это замечательно, что вы взяли на себя труд почитать немного MSDN на предмет APC. Мне искренне жаль разрушать ваши иллюзии… и, возможно, я вас немного разочарую. Но, APC –!!! та-да!!! – ничего не _ждет_. Как бы вам этого не хотелось.
                            — то, чем мы с вами заниаемся обсуждая кто-кого ждет напоминает игру слов :). Может у нас разные понимания вопроса, конечно.

                            Если уж вам так хочется – то _ждет_ именно _поток_, в очередь которого мы помещаем объект APC. Причем, если вдруг выясняется, что поток таки ничего не ждет, то _планировщик_, при первой же возможности, ткнет его носом в очередь.
                            — когда мы размещаем объект APC в очереди он не будет обработан потоком пока поток не перейдет в извещающее состояние (alertable state). Это вам не ожидание обработки? А переходит он в такое состояние какраз по функциям SleepEx, SignalObjectAndWait,…
                            Или я упустил какую нибудь важную деталь?

                            Во-вторых, предлагаю взять то, что вам ближе, и показать уже наконец уважаемой аудитории это ваше «асинхронное ожидание» на примере. С подробным разбором для не сведущих: кто, где, чего и как _ждет_, если вас не затруднит. Заранее спасибо.
                            — это с удовольствием, но в последующих статьях. Если читали внимательно, это вступительная статья в серии. Конкретно далее пойдет про async computation expression в F#, MailboxProcessor и TPL Dataflow.
                              0
                              то, чем мы с вами заниаемся обсуждая кто-кого ждет напоминает игру слов :). Может у нас разные понимания вопроса, конечно.

                              Речь идет о вашей фразе:
                              Поэтому есть смысл ожидать асинхронно сообщение — на счет всяких xAsync.

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

                              Ну и пока я не вижу какого-то уж совсем разного «понимания вопроса» :-) Просто пытаюсь понять, что вы имели ввиду.

                              когда мы размещаем объект APC в очереди он не будет обработан потоком пока поток не перейдет в извещающее состояние (alertable state). Это вам не ожидание обработки? А переходит он в такое состояние какраз по функциям SleepEx, SignalObjectAndWait,…

                              Хм… я вас правильно понимаю, что сейчас уже речь идет не о
                              ожидать асинхронно сообщение
                              , а о том, что это сообщение асинхронно ожидает когда его соизволят обработать? Ну ок. Допустим. Но, тогда вас не затруднит показать, как сообщение может ждать _синхронно_? :-)

                              Или я упустил какую нибудь важную деталь?

                              Для низко уровневых (пользовательских) APC, все примерно так и есть. Если же рассматривать APC вообще, то вы забыли про _планировщик_, который, не зависимо от состояния потока, устроит ему волшебный пендель.
                                0
                                Ок. Как я понял из ваших слов Erlang не предоставляет синхронных методов при работе с агентами. Тоесть, что посылка сообщения через !, что получение сообщения — receive, есть неблокирующими операциями. F# предоставляет аналогичные методы Post и Receive у агента (MailboxProcessor). Но также есть такие методы, как PostAndReply, PortAndReplyAsync, TryPostAndReply, PostAndTryAsyncReply. Конечно Erlang-овых двух методов как базиса достаточно, но эти методы зачастую упрощают жизнь. PostAndReply — приводит к синхронному ожиданию — в методе создается обратный канал передачи данных (AsyncReplyChannel), поток лочится пока агент-получатель не пошлет в канал ответ. PortAndReplyAsync (xAsync) метод не лочит поток, а возвращает обьект типа Async, который вы можете запустить в паралельном потоке, либо (опять повторюсь) «асинхронно ожидать». Конкретно тут это означает то, что поток освобождается и отдается пулу потоков, код после вызова метода PortAndReplyAsync размещается в callback и регистрируется в пуле по факту получения ответа от агента-получателя. Возможно понятие «асинхронно ожидать» сильно привязано к пулу потоков и для NodeJs, например, трудно придумать его обьяснения. Для NodeJs идет стартовая регистрация callback-ов (в которых могут регистрироватся новые callback-ки), а после — ожидание событий и вызов этих callback-ов.

                                Уговорили на пример:
                                let webRequest(url: string) = 
                                    async {
                                            let webReq = WebRequest.Create(url) :?> HttpWebRequest
                                            webReq.Method <- "GET"              
                                            use! response = Async.AwaitTask(webReq.GetResponseAsync())  
                                                //ожидания получения ответа асинхронно - последующий код есть callback, хотя выглядит как последовательное выполнение
                                            use responseStream = response.GetResponseStream()
                                            let! bytes = responseStream.AsyncRead(int response.ContentLength)
                                                //асинхронное ожидание результата с сети - последующий код тоже есть callback-ом, который вызовется по получении всех данных
                                            bytes 
                                            |> System.Text.Encoding.UTF8.GetString
                                            |> printfn "%A" 
                                        }
                                
                                webRequest "http://ya.ru" |> Async.Start //пример запуска асинхронного вычисления в новом потоке
                                


                                Но, тогда вас не затруднит показать, как сообщение может ждать _синхронно_? :-)
                                — сообщение не может ждать синхронно. В данном плане «асинхронно ожидает» означает что пока сообщение находится в очереди, поток может делать свои операции, пока не дайдет до вызова все тех же SleepEx,… Интересно что в F# нет синхронного метода Receive, но в TPL Dataflow есть.

                                Если же рассматривать APC вообще, то вы забыли про _планировщик_, который, не зависимо от состояния потока, устроит ему волшебный пендель.
                                — если я его не упоминал, это не значит что я его забыл. Почему вы говорите что независимо от состояния потока? По доке, для юзер моде APC поток должен находится в alertable state, что б начать обрабатывать
                                APC функцию. Детальней можете обьяснить?
                                  0
                                  Ок. Как я понял из ваших слов Erlang не предоставляет синхронных методов при работе с агентами. Тоесть, что посылка сообщения через !, что получение сообщения — receive, есть неблокирующими операциями.


                                  :-) Вы, скорее всего, не правильно поняли.

                                  Как раз и send operator и recevie expression блокируют процесс. Т.е. что в ходе посылки, что входе приема сообщения процесс ничем другим не занимается.

                                  Другое дело, что сама по себе посылка операция весьма легкая. И её в можно рассматривать как асинхронную (по отношению к посылающему процессу)… ну в ровно в такой же степени, как ф-цию постановки объекта APC в очередь.

                                  А вот receive — синхронный в чистом виде. И никакого асинхронного receive нет. Более того, он и не нужен :-) Для асинхронного взаимодействия (т.н. cast) вполне достаточно того, что «операции» отправки и получения сообщения явным образом разделены. Между ними я могу делать все что мне угодно. А синхронное взаимодействие (т.н. call) описывается последовательностью «послать запрос»/«дождаться ответа». Т.е. послал запрос и тут же встал в receive.

                                  И тут нет, никакого магического «асинхронного ожидания». Ожидание (receive) вполне себе синхронное.

                                  Собственно, ровно по такой же схеме организован асинхронный I/O. Мне никто не запретит после вызова какого-нибудь ReadFileEx тут же проваливаться в «тревожное ожидание». И получится синхронный I/O :-)

                                  Но и тут нет, никакого магического «асинхронного ожидания». Ожидание (вызов какой-нибудь MsgWaitForMultipleObjectsEx) тоже вполне себе синхронное.

                                  Конкретно тут это означает то, что поток освобождается и отдается пулу потоков, код после вызова метода PortAndReplyAsync размещается в callback и регистрируется в пуле по факту получения ответа от агента-получателя.

                                  «Чудес не бывает» (с). То что, в ожидание провалится не поток из пула, а, например, поток диспетчеризации (который реализует event loop) не делает это ожидание асинхронным :-)

                                  По вашему коду. Нет там никаких callback'ов и вообще никакой «магии» с ожиданиями. В F# с этим вообще все просто. Компилятор просто «перепишет» его примерно так:

                                   async.Delay(fun() ->
                                    let webReq = WebRequest.Create(url) :?> HttpWebRequest
                                    webReq.Method <- "GET"              
                                    async.Bind(webReq.GetResponseAsync(), fun response ->
                                      use responseStream = response.GetResponseStream()
                                      async.Bind(responseStream.AsyncRead(int response.ContentLength), fun bytes ->
                                        bytes 
                                        |> System.Text.Encoding.UTF8.GetString
                                        |> printfn "%A")))
                                  


                                  Вы путаете callback и continuation. Не являюсь спецом в F#… писал «от руки». Но, надеюсь, вы поймете о чем речь.

                                  если я его не упоминал, это не значит что я его забыл. Почему вы говорите что независимо от состояния потока? По доке, для юзер моде APC поток должен находится в alertable state, что б начать обрабатывать
                                  APC функцию. Детальней можете обьяснить?

                                  Уф… дай Бог памяти :-) То, что вы описали действительно верно… но только для user-mode APC.
                                    0
                                    Правильно заметели — let! + do!… — F#-сахарок — пеабразуется в вызовы методов. Но для async монады continuation и будет callback-ом. Для других — нет. Все зависит от типа монады (отдельная статья для хабра). Но опять же, наш диалог сводится к разным трактовкам одних и тех же явлений (игру слов).
                                      0
                                      Извиняюсь… малость выпал по работе.

                                      Но для async монады continuation и будет callback-ом

                                      Не возьмусь утверждать… но, лично мне не видно причин, по которым — пусть в даже async контексте — continuation _безусловно_ должен «превращаться» в callback. Вы уверены в своих словах? Это же достаточно просто проверить.

                                      Конкретно в вашем примере, я вижу только одно место — где такое превращение _может быть_. И то, это никак не связано напрямую с F#-овским async.

                                      Связано это со спецификой обработки «стандартной» Task<T>. Там да… в зависимости от SynchronizationContext _может_ потребоваться исполнение continuation в том же самом потоке. Со всеми вытекающими, как говорится.
                                        0
                                        Не возьмусь утверждать… но, лично мне не видно причин, по которым — пусть в даже async контексте — continuation _безусловно_ должен «превращаться» в callback. Вы уверены в своих словах? Это же достаточно просто проверить.
                                        — отпринтим айди потока в разных точках асинка
                                        let webRequest(url: string) = 
                                            async {
                                                    printfn "Point 0 in thread %d" System.Threading.Thread.CurrentThread.ManagedThreadId
                                                    let webReq = WebRequest.Create(url) :?> HttpWebRequest
                                                    webReq.Method <- "GET"              
                                                    use! response = Async.AwaitTask(webReq.GetResponseAsync())  
                                        
                                                    printfn "Point 1 in thread %d" System.Threading.Thread.CurrentThread.ManagedThreadId
                                                        //ожидания получения ответа асинхронно - последующий код есть callback, хотя выглядит как последовательное выполнение
                                                    use responseStream = response.GetResponseStream()
                                                    let! bytes = responseStream.AsyncRead(int response.ContentLength)
                                        
                                                    printfn "Point 2 in thread %d" System.Threading.Thread.CurrentThread.ManagedThreadId
                                                        //асинхронное ожидание результата с сети - последующий код тоже есть callback-ом, который вызовется по получении всех данных
                                                    bytes 
                                                    |> System.Text.Encoding.UTF8.GetString
                                                    |> printfn "%A" 
                                                }
                                        webRequest "http://ya.ru" |> Async.Start //пример запуска асинхронного вычисления в новом потоке
                                        


                                        на моей машине результат:

                                        Point 0 in thread 12
                                        Point 1 in thread 11
                                        Point 2 in thread 7

                                        — разные потоки. Детальные пояснения если вам интересно почитайте в книге Дона Сайма — создателя языка.
                                        В разделе Understanding Thread Hopping конкретно обьяснено почему continuation есть callback-ом. Сама регистрация последнего идет в методе AsyncBuilder.Bind (в вашем коде сверху).

                                        Связано это со спецификой обработки «стандартной» Task. Там да… в зависимости от SynchronizationContext _может_ потребоваться исполнение continuation в том же самом потоке. Со всеми вытекающими, как говорится.
                                        — async вычисление может быть запущено или продолжено в разных контекстах — смотрите Async модуль, методы SwitchToContext, SwitchToNewThread…
                                          0
                                          на моей машине результат:

                                          Point 0 in thread 12
                                          Point 1 in thread 11
                                          Point 2 in thread 7


                                          Собственно, о чем и речь. Где вы тут callback'и увидели?! Или давайте тогда так… под термином callback вы что именно понимаете? А то я старый человек — для меня это вполне конкретный термин :-) Просто, очень похоже на то, что callback'ом вы называете любую передачу ф-ции как аргумента. И если это так, то предлагаю на этом завершить наш разговор… тогда мне становится понятно за ваше «асинхронное ожидание» :-)

                                          В разделе Understanding Thread Hopping конкретно обьяснено почему continuation есть callback-ом.

                                          А вас не затруднит процитировать? А то я перечитал этот раздел уже раза три. Единственное место, где упоминается callback:
                                          When the requests complete, they trigger a callback in the .NET thread pool. These may
                                          be serviced by different threads than those that originated the calls.

                                          Это и есть " конкретное объяснение"?! Если таки да, то это верно лишь для т.н. SynchronizationContext по умолчанию. Сама по себе F#'кая async вполне может только на него и опираться, но Async.AwaitTask (т.к. работает с Task) такого позволить себе не может.

                                          async вычисление может быть запущено или продолжено в разных контекстах

                                          Речь не о async самой по себе. Речь о том, что кто именно (какой именно поток, если хотите) — а, по большому счету, и как именно — будет вычислять ф-цию, которая должна быть вычислена «после того как» определяется как раз контекстом синхронизации. По умолчанию — всё тупо «сваливается» на ThreadPool. Но, это не значит, что так происходит всегда.
                                            0
                                            Единственное место, где упоминается
                                            — ок, чуть дальше в книге

                                            1. Async<'a> values are essentially a way of writing “continuation-passing” or “callback” programs
                                            explicitly

                                            2. The use of let! executes this operation asynchronously and registers
                                            a callback. When the callback is invoked, the value pixels is bound to the result of the operation,
                                            and the remainder of the asynchronous workflow is executed.

                                            Если таки да, то это верно лишь для т.н. SynchronizationContext по умолчанию
                                            — так оно и есть. Для других контекстов юзайте SwitchToContext, SwitchToNewThread, AwaitTask…
                                            В общем книга вам в руки.
            +2
            Если бы это был студенческий реферат, то больше тройки я бы не поставил.

            0. Повсевместное использование «message passing» вместо «передача сообщений» вызывает смутные подозрения.

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

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

            3. Поскольку передаче сообщений нет альтернативы, то не понятно, в сравнении с чем описываются «особенности message passing».

            4. Передача и прием сообщений могут быть организованы по разному. Хотелось бы видеть классификацию разных способов.

            5. Наиболее интересная проблема в обработке сообщений — противоречие между асинхронностью и параллелизмом. Попытка исполнять callback'и параллельно часто заканчивается провалом. Причина в прямолинейном сопоставлении сообщение -> реакция, в то время как реакция должна выполняться только при поступлении полного набора токенов, как в сетях Петри.
              0
              На счет терминологии — вы правы. Приму к сведенью в следующих статьях.
              На счет пункта 2 и 3. В данном случае рассматриваются использование агентов не только в рамках компьютерной сети, но и в рамках одного процесса на одной машине. В данном случае использование агентов составляет конкуренцию использованию мьютексов, семафоров, критических секций и т.д для доступа к общему ресурсу, поскольку ресурс пренадлежит агенту (его потоку) и получить ресурс можно только через сообщения. Для распределенных систем (сетевого приложения) действительно существует только передача сообщений через сокеты. На основе этого подхода формируються более серьезные технологии – REST, SOAP сервисы.

                0
                Если вы хотели противопоставить Task-based parallelism и программирование с помощью потоков управления (threads), то, с одной стороны, потоки также могут обмениваться сообщениями (через блокирующие очереди), а сдругой стороны — таски (агенты в вашей терминологииФ) могут обмениваться сигналами, то есть сообщениями без информации, и эти сигналы точно соответствуют семафорам в многопоточном программировании.
                  0
                  потоки также могут обмениваться сообщениями (через блокирующие очереди)
                  — BufferBlock в TPL Dataflow

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

                  эти сигналы точно соответствуют семафорам в многопоточном программировании.
                  — в каком смысле соответствуют? Семафо́р — объект, ограничивающий количество потоков, которые могут войти в заданный участок кода. Можете дать какие-нибудь ссылки по этому вопросу?
                    0
                    в каком смысле соответствуют? Семафо́р — объект, ограничивающий количество потоков, которые могут войти в заданный участок кода. Можете дать какие-нибудь ссылки по этому вопросу?


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

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

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

            Самое читаемое