Hopac
-- самостоятельный асинхронный движок, написанный специально под F#.
Он стоит на 4 китах, одним из которых является перенаправление потоков вычисления через явное противопоставление конкурирующих задач. Конкурирующие задачи (или ветки) реализуются через концепцию альтернатив (или Alt
), которую я хочу осветить в этом цикле из трёх статей.
Я не буду объяснять Hopac
с нуля. Несложно найти гайды по азам в англоязычном сегменте сети. Я, конечно, дам пояснения по большинству понятий, чтобы подстраховать и себя, и читателя. Но в первую очередь я хочу решить проблему тех, кто сталкивался с Hopac
или даже писал что-то в прод, однако остался неудовлетворённым своим уровнем осознания происходящего. Как мне кажется, доля таких F#-истов довольно большая. Ибо Hopac
при наличии адекватных задаче примеров позволяет выдавать результат сильно раньше, чем приходит понимание происходящего под ковром.
Нужно подчеркнуть, что у Hopac
есть:
Довольно увесистая документация. И зачастую её достаточно как для ситуативного поиска, так и для изучения потоком.
Большущий обзорный гайд (соразмерный данному циклу), но который по сложности и применимости примеров почему-то начинает не с того конца.
Набор статеек поменьше на произвольные темы.
Однако природа Alt
оказалась слишком сложной (ну, или конкретно у меня так не срослось), поэтому, несмотря на вроде бы исчерпывающее описание, для меня работа с Hopac
в первые год-два регулярно оборачивалась новыми открытиями, как приятными, так и не очень. Так что эта статья создавалась как альтернативный кодекс по Alt
. Лёгких приключения здесь не ожидается, ибо материал местами довольно сложный. Упрощать его -- значит, написать очередную статью, после которой придётся выковыривать недостающие элементы в самых диких локациях интернета. Занятие это на любителя, к коим я себя не отношу. Тем не менее жертву сию я некогда принёс и считаю возможным избежать её "последующими поколениями". Поэтому я сосредоточусь на "тотальном" описании всего и вся, сознательно жертвуя локальной последовательностью повествования.
В первой части мы ознакомимся с самыми примитивными понятиями. Коснёмся готовых
Alt
-ов, что были предоставлены самимHopac
. Научимся их противопоставлять. И разберёмся с понятием коммита, где он находится и для чего он нужен на практике.Во второй части перейдём от песочницы к реальным примерам. Будем из базовых примитивов строить
Alt
-ы посложнее. Для подавляющего большинства этого уровня хватит, чтобы выполнять повседневные задачи.В третьей части мы пройдёмся по нечастым, но важным категориям. Научимся правильно "откатывать" неудачные альтернативы, ловить и выбрасывать исключения.
Пара предупреждений о коде
Я ориентируюсь на версию 0.5.0
(на момент написания последней была 0.5.1
).
Тем, кто собирается держать REPL
сессию под рукой:
#r "nuget: Hopac, 0.5.0"
В Hopac
есть модуль Hopac.Infixes
.
В нём определены операторы над типами Hopac
.
Операторов довольно много, но лишь небольшая часть из них требуется на более-менее постоянной основе.
В любом случае для работы кода вам потребуется открыть:
open Hopac
open Hopac.Infixes
Я адепт "крышечки".
Этот оператор экономит некоторое количество скобочек за счёт игр с приоритетом выполнения.
Крышечки нет в Hopac.Infixes
, хотя она и используется в исходниках Hopac
.
Для того, чтобы код работал, вам потребуется определить:
let inline (^) f x = f x
Что есть Alt
Каждый F#-ист к моменту чтения статьи должен представлять себе "изкоробочный" 'a Async
.
Поэтому он легко сможет усвоить, что:
'a Job
-- это базовая единица асинхронной операции вHopac
.Она обладает идентичным с
'a Async
-ом поведением, за исключением неявногоCancellationToken
. Его нет, работайте руками.
В остальном это такой же холодный 'a Task
, но с другим именем (момент с запуском Task
пропустим). Job
имеет целый сонм наследников на все случаи жизни. Одним из таких потомков является 'a Alt
, который мы будем разбирать далее. Поддерживает поведение Job
и даёт несколько плюшек сверху.
Alt
является сокращением от Alternative
. Существование нескольких альтернатив порождает возможность выбора. Но Hopac
не просто запускает несколько альтернатив и забирает результат из первой завершившейся. Он позволяет альтернативам:
реагировать на выбор других альтернатив;
сообщать заранее о предстоящей готовности, чтобы другие смогли остановиться как можно раньше.
Из-за этого реализация Alt
имеет очень нетривиальное исполнение. Поэтому, если не считать вырожденных случаев, то создать Alt
с нуля без опоры на другой Alt
технологически невозможно. В отличие от автономного Job
Alt
является лишь общим интерфейсом над конкретными потомками. Потомки хоть и не запечатаны, но их поведение переписать практически невозможно, только навесить дополнительные карманы.
Откуда берутся Alt-ы
Имеется следующая иерархия типов:
Схема может выглядеть довольно увесисто, если сравнивать с аналогами. Но типы под Alt
автономны и не содержат каких-либо ссылок друг на друга (как минимум в публичном API).
Благодаря этому можно осваивать типы, отталкиваясь от существующей кодовой базы, игнорируя параллельные ветви. Так что даже сейчас я очень поверхностно представляю природу Latch
и Proc
. Этого же подхода я буду придерживаться и по ходу статьи. Информация о данных типах будет поступать по мере необходимости.
Каждый из этих типов имеет одноимённый модуль, в котором обязательно есть "основная" функция, которая преобразует их в обычный Alt
. Имя её зависит от семантики типа. Например, IVar.read
или Mailbox.take
. Каждый из этих типов может неявно каститься в Alt
.
Но сущностно, данный каст будет вызывать "основную" функцию из модуля. Следующие альтернативы будут идентичны по наполнению.
let read : _ Alt = IVar.read ^ IVar()
let alt : _ Alt = IVar()
Оба варианта сохранят под "капотом" информацию об исходном типе. Так что на самом деле IVar.read
и ему подобные нужны не для реального создания Alt
, а для вывода типа компилятором.
(IVar.read ^ IVar<int>()) :? IVar<int> // true
(IVar<int>() :> _ Alt) :? IVar<int> // true
Для сокрытия данной информации есть функция Alt.paranoid : 'a Alt -> 'a Alt
. Она создаёт действительно новую альтернативу на базе предыдущей. Её задача -- исключить риск обратного каста к исходному типу.
let paranoid : _ Alt = Alt.paranoid ^ IVar()
(Alt.paranoid ^ IVar<int>()) :? IVar<int> // false
При проектировании типов и модулей на основе Hopac
вы вряд ли будете светить конкретные типы в публичном API. Можно сказать, что на каждые 10 публичных упоминаний типов из представленной иерархии у вас будет:
5 упоминаний
Job
.1 упоминание
Alt
.2 упоминания
Promise
(это какlazy
, но на базеJob
, т.е.memo ^ job { return 42 }
вместоlazy(42)
).
Оставшиеся 2 могут касаться особых случаев (если они есть), порождённых конкретным доменом. В моих кодовых базах это, скорее всего, будет IVar
(ячейка, которую можно заполнить только один раз и навсегда) и Mailbox
. В чужих же репах я наблюдаю лидерство за Ch
(от Channel
, родня System.Threading.Channels
).
Серьёзное влияние на данное распределение оказывает наличие или отсутствие 'a Hopac.Stream
. Наиболее близкой его аналогией будут Rx
, AsyncSeq
, IAsyncEnumerable
и System.Threading.Channels
(ещё раз), с явным преимуществом за Stream
. Присутствие Stream
зачастую обессмысливает использование Ch
и Mailbox
. А также сводит наше распределение типов к дихотомии Job
vs Promise
. (Замечу, что сам Stream
, а точнее сопутствующая ему машинерия, основаны на Alt
.)
Малое число упоминаний в публичном поле не тождественно низкой частоте использования Alt
. Из пачки альтернатив можно собрать хороший раздражитель для конвейера, что будет работать автономно в своём режиме. Всё его бытие можно будет описать в виде реакций на конкретные события во внешней системе (кстати, в CML предок Alt
известен как Event
).
Hopac
позволяет довольно легко писать акторо-подобные сущности, запущенные в пустоту.
(Акторы без акторной системы некорректно называть акторами, поэтому в терминологии Hopac
они называются серверами
. В моей ойкумене термин сервер
не особо прижился, и если рядом в тексте/коде не используется какой-нибудь Job.foreverServer
, мы используем термин "актор".)
Если ваш актор просто совершает рутинные операции над потоком сообщений, то вполне возможно, что вы спокойно ограничитесь Job
. Однако любая нелинейная обработка сообщений будет построена на конфликте различных альтернатив. В большинстве случаев первой вашей альтернативой обработчику сообщений по умолчанию будет команда на остановку обработчика. Так называемый Poison Pill
, который встречается в различных акторных системах. В MailboxProcessor
, Akka
и т. п. реализациях убийство актора часто сопровождается приключениями, что приводит к бесконечному потоку гайдов по подводным камням. Hopac
этой проблемы избежал через использование другой сетки абстракций, системообразующим элементом которой является Alt
. И поэтому мы сбацали ещё один гайд...
Немного примеров
Отложенная инициализация
Мне необходимо писать приложения под конечных пользователей на различных устройствах.
Это означает, что часть имплементаций будет дана за пределами ядра, на конкретных платформах. Если речь идёт о мобилках, то реализации может вообще не быть. Иногда реализация может задерживаться на минуты. Для всего выше перечисленного часто используется IVar
.
IVar.fill
заполнит ячейку в зоне старта приложения.IVar.read
/Alt.paranoid
позволит привязаться к операции заполнения на стороне ядра. Читатель получит значение, как только оно будет заполнено. Если у него нет желания или возможности ждать, то он сможет решить проблему через композицию (к середине второй статьи станет ясно как).
В самом простом случае мы имеем дело с обычной незащищённой ячейкой let instance = IVar<Api>()
. И конечный пользователь будет работать с ней напрямую. Однако, если инициализация будет сопровождаться какими-то дополнительными операциями или предваряться валидацией, то возможно разделение на read
/ write
блоки.
let private instance = IVar<Api>()
module PublicApi =
// jobResult импортирован из FsToolkit.ErrorHandling.JobResult
let tryInit props = jobResult {
do! validate props ...
do! IVar.fill instance
}
let wait = Alt.paranoid instance // : Api Alt
Актор
Акторы/сервера -- чуть более сложный случай. Рычагов и прочих крутилок у них заметно больше. Есть контроль жизненного цикла. Есть очередь входных сообщений, иногда не одна.
Изредка отдельно выносят управление параметрами обработки и т. п. Даже в минимальной версии потребуется изолировать:
Почтовый ящик, чтобы, кроме нас, из него никто ничего не забрал.
Флаг о том, что актор окончательно умер, чтобы его ложно не поднял кто-то другой.
Флаг о том, что актор попросили умереть, чтобы его не спутали с
terminated
.
// : 'message Mailbox
let private msgs = Mailbox()
// : unit IVar
let private poisonPill = IVar()
// : unit IVar
let private terminated = IVar()
start ^ ... // Какая-то server магия.
module PublicApi =
// : 'message -> unit Job
let sendMessage msg = Mailbox.send msgs msg
// : unit Job
let kill = IVar.tryFill poisonPill ()
// : unit Alt
let terminated = Alt.paranoid terminated
Здесь может возникнуть ощущение зыбкости. Оно пройдёт по мере развития сюжета. Hopac
весь построен на таких маленьких элементах. Стягивать эту рыхлую конструкцию воедино будут обручи из жёсткого контроля доступа к событиям и из реакций на те же события внутри системы.
Вырожденные случаи
Отталкиваясь от приведённых примеров, можно заметить, что для Hopac
характерно раннее деструктурирование сложных объектов. В отличие от C# здесь вам вряд ли выдадут интерфейс с 10 методами. Либа подталкивает использовать CQRS подход. Зачастую команды и запросы будут перекидываться по системе в качестве самостоятельных элементов. В отрыве от сущностей, которым они принадлежат.
Схожую процедуру разрыва претерпели вырожденные случаи Alt
. Под капотом они могут являться как самостоятельными наследниками Alt
, так и результатом композиции. Однако для внешнего наблюдателя не существует никаких рычагов воздействия на них, кроме прямого запуска. То есть у данных кейсов наличествует только Query составляющая, да ещё и в единственном числе. Поэтому работа с такими случаями была упакована в одинокие функции, большинство из которых лежит в модуле Alt
.
Alt.never : unit -> 'a Alt
-- никогда не завершается, тянет резину вечно.Alt.always : 'a -> 'a Alt
-- всегда по первому требованию выдаёт указанный результат.Alt.once : 'a -> 'a Alt
-- в первый раз выдаст указанный результат, а потом превратится вnever
.Alt.fromAsync : 'a Async -> 'a Alt
-- строитAlt
на базеAsync
, и если потребуется, воспользуется внутреннимCancellationToken
-ом.Alt.fromTask: (CancellationToken -> 'a Task) -> 'a Alt
-- то же самое для'a Task
, но с явной передачей токена. ЕстьAlt.fromUnitTask
для необобщённогоTask
.timeOutMillis : int -> unit Alt
-- аналогAsync.Sleep
, естьtimeOut
дляTimeSpan
.Редкие кейсы типа
Alt.raises
,Alt.unit
,Alt.zero
и что-то ещё по закоулкам.
Alt как Job
Все особенности Alt
-а раскрываются во взаимодействии с другими Alt
-ами. И запуск отдельно стоящего экземпляра ничего сверх Job
не даст.
let ivar = IVar()
...
// Будет ждать, пока кто-то не заполнит значение ivar в другом потоке.
let foo = run ivar
Это касается всех случаев употребления Alt
как Job
. Следует запомнить, что данная деградация является билетом в один конец (если не касаться ручных кастов). Как только система потеряет информацию об альтернативе, фарш прокрутить назад будет невозможно.
Из этого следует, что лучше преобразовывать Alt
-ы в Alt
-ы как можно дольше, не скатываясь в Job
.
Интересно, что некоторые из Alt
могут сообщать остальным, что их ждать бесполезно.
Например, Alt.never
(или Alt.once
после своей разрядки) будут очень активно перенаправлять выбор от себя. Это даёт некоторый бонус по производительности. Однако на это свойство нельзя опираться при работе с ними как с обычными Job
. Вы никогда не дождётесь окончания run ^ Alt.never()
.
Alt vs Alt
Hopac
имеет 5 хелперов для выбора между несколькими альтернативами. Однако сущностно они разделяются на две группы. Выбор может быть упорядоченным или случайным.
Упорядоченный выбор
Нужен, когда мы можем выстроить альтернативы в порядке убывания приоритета. Предполагается, что чем выше важность альтернативы, тем скорее необходимо перейти к её обработке. Правда, когда порядок альтернатив абсолютно не важен, данный способ тоже подходит.
val (<|>) : `a Alt -> `a Alt -> `a Alt
(<|>)
-- наиболее распространенный хелпер (канонического названия не обнаружил, в локальных обсуждениях упоминается как "или"). Запускает один или оба Alt
и возвращает результат того, что реализуется быстрее. Позволяет без особого ущерба для производительности сворачивать целые цепочки Alt
-ов в один.
let uber = // : _ Alt
a <|> b <|> c <|> d ...
Этот оператор опрашивает альтернативы слева направо. Это не особо влияет, если на момент запроса обе альтернативы не имеют готового результата. Однако этого хватает, чтобы детерминированно разрешать мемоизированные кейсы (Promise
). Именно на этом принципе построен Hopac.Stream
.
В целом рекомендовал бы чтение исходников Stream.fs
для познания Hopac
-дзена. С некоторой поправкой на любовь автора Hopac
к очень длинным кастомным операторам и ужасным отступам.
Если альтернатив становится много, или их общее число неизвестно на этапе компиляции, то используются:
module Alt =
val choose: seq<#Alt<'a>> -> Alt<'a>
val choosy: array<#Alt<'a>> -> Alt<'a>
В обеих функциях: чем ближе к началу окажется входная альтернатива, тем больший у неё шанс стать "той самой" за счёт более раннего старта. Разница между двумя функциями лишь в области перфоманса. Alt.choosy
немного быстрее на больших объёмах. Однако дельта столь мала, что меня эта проблема никогда особо не беспокоила, так что Alt.choose
-- one love.
Случайный выбор
val (<~>) : `a Alt -> `a Alt -> `a Alt
Аналог (<|>)
, но опрос производится случайным образом.
module Alt =
val chooser: seq<#Alt<'x>> -> Alt<'x>
Аналог Alt.choose
с рандомным выбором. "Ускоренной" версии типа Alt.choosy
не завезли.
До написания этой статьи я пробовал использовать случайный выбор лишь для рандомных действий в области бизнес-логики. Почти всегда мне было необходимо воспроизводить "случайный выбор" в рамках тестов и т. п. Поэтому я чаще самостоятельно рандомизировал коллекцию в зависимости от внешнего seed
. Так что в моих планах было написать о практической бесполезности такого подхода.
Однако в процессе обсуждения выяснилось, что случайный выбор позволяет балансировать несколько конкурирующих очередей средствами Hopac
. Идея кажется мне интересной, но опыта её применения у меня нет. До этого я решал её либо ручной балансировкой на базе того же Hopac
, либо средствами соответствующего провайдера очередей.
Alt.choose vs Task.WaitAny
К этому моменту, в голове читателя, Alt
из этого очень условного определения:
type 'a Alt = 'a Job
Мог эволюционировать до следующего, не менее условного:
type 'a Alt = 'a option Job
В любом случае выдуманная опорная версия choose
должна была выглядеть как-то так:
let choose alts = job {
let result = IVar()
for alt in alts do
do! Job.queue ^ job {
match! alt with
| None -> ()
| Some res -> do! IVar.tryFill result res
}
return! result
}
Глядя на эту симуляцию, может возникнуть вопрос, в чём принципиальная разница между Alt.choose
и каким-нибудь Task.WaitAny
. (Выдёргивание результата по индексу пропустим.)
Во-первых, выбор итоговой альтернативы может происходить до того, как она успела доделать все необходимые операции. Это возможно при условии, что данная альтернатива дала гарантии своего завершения. Во-вторых, надлежит вспомнить, что попытка выполнить Alt
не является строго однонаправленной операцией. И этот выбор сопровождается актом коммита. В ходе него:
Alt.choose
(или его аналог) фиксируется на определённой альтернативе, о чём ей и сообщает.Альтернативы, что не были запущены до коммита, не запускаются вовсе.
Оставшиеся альтернативы, что были запущены до коммита, получают извещение о том, что в их результате больше нет необходимости. Реакция на данное извещение -- дело конкретных имплементаций
Alt
.
После этого Alt.choose
"доделывает" выбранную альтернативу.
Если облечь необходимые нам узлы в код, то потребуется приблизительно такая конструкция:
type 'a CommittedAlt = 'a Job
type 'a ReadyAlt = 'a CommittedAlt Job
type 'a StartedAlt = {
Abort : unit Job
Wait : 'a ReadyAlt option Job
}
type 'a Alt = 'a StartedAlt Job
Мне сложно воспринимать подобные цепочки типов в отрыве от дела. Так что сразу сосредоточимся на применении:
let choose (alts : 'a Alt seq) = job {
let firstReady = IVar() // : 'a ReadyAlt IVar
let committedId = IVar() // : Id IVar
for alt in alts do
if not firstReady.Full then
do! Job.start ^ job {
let! started = alt // : 'a StartedAlt
let thisId = genId ()
do! Job.queue ^ job {
let! committedId = committedId
if committedId <> thisId then
do! started.Abort
}
match! started.Wait with
| None ->
do ()
| Some ready ->
do! IVar.tryFill firstReady (ready, thisId)
}
let! (ready, sourceId) = firstReady // 'a ReadyAlt * Id
let! afterCommit = ready // 'a CommittedAlt
do! IVar.fill committedId sourceId
return! afterCommit
}
Этот код не похож на тот, что содержится в исходниках. В них вы найдёте крайне запутанную систему написанную на C# (sic!), с использованием Interlocked
-ов на километровых расстояниях. Но данный пример хорошо отражает общий ход операции, и его придётся понять, ибо он определяет категориальный аппарат на оставшуюся часть статьи.
Здесь у нас есть 2 "глобальные" точки, важные для всех альтернатив.
firstReady.Full
-- факт готовности первой альтернативы. Если это произошло, то дальнейший запуск альтернатив прекращается.committedId
-- известие с идентификатором закоммиченной альтернативы. Оно провоцирует прекращение вычисления всех альтернатив, оставшихся не у дел.
В рамках данного примера firstReady
и committedId
можно было бы объединить. Но это возможно лишь благодаря тому, что наш choose
возвращает Job
, а не Alt
, как должен был. В реальности существует вероятность, что firstReady
будет не востребована из-за того, что внешняя альтернатива справится быстрее. В этом случае над всеми присутствующими, включая firstReady
, будет вызван abort
.
Если идти до конца, то потребуется одну большую неостановимую Job
распилить на 4, две из которых способны откатываться к своему началу. Система обработки исключений сюда тоже не попала. Как и сложности подготовки коммита. Я счёл вредным заходить так далеко в рамках этой статьи.
Иногда можно проще
Мне нравится приведённая симуляция из примера выше, но мне не нравится то, как с нею обращаются новички. Слишком большое внимание к технической реализации. Вплоть до пошагового прохождения по коду. На самом деле, почти всегда, вам будет достаточно следующей модели:
type 'a Alt = 'a Job option Job
Грубо, игнорирует большую часть из сказанного в данном цикле, но на макроуровне это будет работать почти всегда.
Ценность коммита
Ранее я упоминал про Alt.once
.
Alt.once : 'a -> 'a Alt
-- в первый раз выдаст указанный результат, а потом превратится вnever
.
Согласно этому определению, следует ожидать следующего поведения:
let once = Alt.once 42
// Выведет: First result: 42
printfn "First result: %i" ^ run once
// Зависнет навечно.
printfn "Second result: %i" ^ run once
Ошибочно можно было предполагать следующую имплементацию:
let once =
let mutable committed = false
job {
if committed
then return! Job.never()
else return 42
}
|> магия, превращающая Job в Alt
И она даже пройдёт "тест" из примера выше.
Верно, что у данного Alt
есть скрытое состояние, и оно может измениться в результате запуска. Но в нашей ложной имплементации изменение будет произведено вообще при любом запуске. В то время как реальный Alt.once
может измениться только при прохождении через коммит.
Запуск без коммита возможен только в случае параллельного запуска нескольких альтернатив. В проде Alt.once
чаще всего отвечает за операции инициализации, что должны быть выполнены только один раз и как можно раньше. Однако у нас пока нет необходимого инструментария, так что возьмём абсолютно синтетический пример:
let one = Alt.once 1
let two = Alt.once 2
let total = one <|> two <|> Alt.always 42
for index in Seq.initInfinite id do
// Выведет:
// #1 result: 1
// #2 result: 2
// #3 result: 42
// #4 result: 42
...
printfn "#%i result: %i" index ^ run total
Здесь мы бесконечно запускаем один и тот же total : int Alt
и выводим его результат в консоль. Он состоит из двух Alt.once
, каждый из которых должен отстреляться ровно по одному разу, и одного Alt.always
, который будет затыкать дыры после разрядки остальных.
Если бы любой запуск Alt.once
приводил к изменению состояния, то мы бы получили чуть менее разнообразный вывод. Так как two
рисковал бы потерять своё содержимое уже при первом запуске total
.
#1 result: 1
#2 result: 42 // vs #2 result: 2
#3 result: 42
#4 result: 42
(Может быть полезно в контексте GC: Alt.once
теряет ссылку на контент после первого коммита.)
Если взять 'x Ch
(далее Ch
), как наиболее полный модельный пример Alt
, то обе операции отправки (Ch.give
) и получения (Ch.take
) сообщения являются Alt
-ами. Для коммита альтернативы потребуется синхронизация отправителя и получателя в одной точке. Только в этом случае они произведут атомарную операцию передачи.
Поэтому запись вида:
// Канал с обычными сообщениями.
let msgs = Ch()
// Канал с важными сообщениями, обработка которых должна идти в приоритете.
let criticalMsgs = Ch()
start ^ Job.foreverServer ^ job {
let! msg =
// Забираем первое доступное сообщение
// с приоритетом за критическими сообщениями.
Ch.take criticalMsgs <|> Ch.take msgs
// можно проще с тем же результатом
// criticalMsgs <|> msgs
printfn "%A" msg
}
Будет бесконечно забирать по одному сообщению ровно из одного канала.
Ни одно сообщение не будет потеряно из-за конкурентной гонки в рамках данного сервера.
Пока в
criticalMsgs
будут сообщения, только они и будут забираться.msgs
сможет вклиниться в процесс только после полного опорожненияcriticalMsgs
.
Все это запрограммировано на "физическом уровне" за счёт наличия акта коммита.
Ибо он производит необратимую операцию лишь над одной альтернативой.
Однако если за пределами нашего сервера будет запущен ещё один получатель на этих же каналах, то часть сообщений будет попадать к этому получателю, а часть к нашему серверу. Причём каждое сообщение также попадёт только в один из обработчиков. Это не всегда плохо и может быть инструментом настройки параллельной обработки.
Промежуточный итог
В этой части мы разобрались с базовыми примитивами. Приблизительно поняли, где они находятся в реальном приложении, а также как глубоко они зарыты. Научились противопоставлять альтернативы друг другу. Арсенал пока не слишком большой, но достаточный, чтобы приносить пользу в виде небольших островков, разбросанных по проекту. В следующей части попробуем затронуть случаи посложнее, приближенные к проду и рассчитанные на более развитые системы.
Автор статьи @kleidemos
НЛО прилетело и оставило здесь промокод для читателей нашего блога:
— 15% на заказ любого VDS (кроме тарифа Прогрев) — HABRFIRSTVDS