Архитектуры на акторах: монолиты

Підписуйтеся на Telegram-канал «DOU #tech», щоб не пропустити нові технічні статті.

                                         (1) Как нарисовать сову?
                                         ------- мы здесь -------
                                         (2) Рисуем кружочки.
                                         (3) Рисуем оставшуюся часть совы.

Продолжим наш core dump

Архитектуры для систем акторов не попадаются в литературе — вероятно, потому, что такие системы крайне неудобно отлаживать (недетерминизм и асинхронная логика). В результате, пока весь цивилизованный мир занимался паттернами для ООП, акторы жили на отшибе где-то в эмбедеде и телекоме (системы управления реального времени). Потом мода на ООП и паттерны прошла, старые известные вещи начали переизобретать и ребрендить, и все ушли в микросервисы, которые отличаются от классических акторов тем, что могут взаимодействовать как синхронно (RPC), так и асинхронно (event sourcing [MP], pub/sub [EIP, POSA1]). Сейчас в каждом мире свой набор паттернов с разными названиями, но примерно одинаковой сутью.

Что дает возможность хоть как-то подойти к попытке систематизировать информацию — это то, что обычно одной из основных целей архитектурных паттернов и в мире ООП, и в мире сервисов, было создание low coupling + high cohesion систем для минимизации сложности (затрат на поддержку), то есть — стремление разбить систему на не очень большое количество относительно независимых компонентов, каждый из которых делал бы одно дело, и делал его хорошо (Unix way [DDIA]). Именно такой цели служат объекты, но и акторы делают то же не хуже (как и (микро-)сервисы). Поэтому мы можем брать классические архитектурные паттерны из мира ООП и из мира сервисов, сравнивать их друг с другом, и пытаться применять к акторам. Чем и займемся.

Но сначала — посмотрим на разницу между control- и data- flow применением:

Control flow vs Data flow

Любая программа пережевывает входящую информацию и выплевывает исходящую.

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

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

Возьмем фотик и могилку.

Смысл жизни фотика — в передаче потока/очереди данных с матрицы на экран или на флеш. Это может выглядеть так:

[Read from matrix to RAM] => [RGGB to RGB] => [Calculate image statistics] => [Apply brightness correction] => [Apply color correction] => [Apply noise reduction] => [Decrease color depth] => (1: [Write to LCD]) or (2: [Write to viewfinder]) or (3: [Compress to JPEG] => [Write to flash]).

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

Это был data flow. Данные проходят кучу этапов, как в мясорубке, но сама схема не меняется.

Теперь берем мобилку. Когда пользователь нажимает клавишу 2, могут произойти следующие действия:

  • если сейчас установлена аудио связь, в радиомодуль будет отослано сообщение с набранной цифрой, а также будет включен генератор DTMF для обратной связи.
  • если сейчас на экране набор текста, и недавно была нажата клавиша 2, то изменяется последний набранный символ.
  • иначе если на экране набор текста, к тексту добавляется буква A или a или цифра 2, в зависимости от состояния.
  • если на экране меню, то будет выбран второй пункт меню.
  • если на экране змейка, то она заворачивает вверх.
  • если играет будильник, мерзкое пищание прекратится на 5 минут.
  • если клавиатура заблокирована, и недавно не было нажато клавишей, на экране начнет воспроизводиться анимация о том, как разблокировать клавиатуру.
  • иначе, если анимация уже воспроизводится, ничего не произойдет.
  • если телефон выключен (на самом деле — он включен, но на пониженной частоте), то ничего не произойдет. Он ждет, пока нажмут кнопку питания, и не отпустят ее несколько тактов.

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

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

Это называется control flow. Событие ничего не значит без состояния системы, которое и является решающим фактором, реагировать ли, и каким образом, на входящее сообщение.

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


Data flow


Control flow


Цель


Обработка потока данных


Реакция на события


Эффективность как


Пропускная способность


Время отклика


Распространение сообщения по системе


Одинаково для каждого сообщения одного типа


Различается для каждого инстанса сообщения


Сообщения меняют состояние системы


Обычно нет


Обычно да


Размер интерфейса (количество типов сообщений)


Узкий (единицы или десятки)


Широкий (сотни или тысячи)


Объем данных в сообщении


Обычно большой (пакет данных)


Обычно малый (нотификация о событии)

Теперь мы готовы заняться собственно архитектурой.

Монолит

Сейчас в мире распределенных систем монолитом принято называть набор компонентов, который нужно вместе деплоить. Так было не всегда. В [POSA1] нет самого термина «монолит», но встречается «монолитная система», «монолитное приложение» как противоположность слоистым и распределенным архитектурам. В таком значении и будем использовать этот термин: назовем монолитом то, начинку чего не получается понять. Так как мы сейчас исследуем асинхронные (messaging) интерфейсы — разбитие системы на акторов — то синхронные куски (отдельные акторы) для нас более-менее похожи на монолиты. Будем к ним в дальнейшем так относиться. Итак: что же у нас внутри актора?

Монолит — самый простой и очевидный вариант, который классики [MP] рекомендуют к использованию, когда:

  1. не особо знакомый домен или
  2. проект не собирается вырасти до проблемных размеров по какому-либо измерению и
  3. нет специальных нефункциональных требований вроде малого времени отклика или высокой отказоустойчивости

Плюсы:

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

Минусы:

  • Весь код связан, и мы не можем добиться существенно разных свойств (нефункциональные требования [MP]) для разных частей программы (субдоменов).
  • Код легко запутывается (poor evolvability).
  • Если падает, то все целиком, и пользователь в шоке.

Забавно, что «падает все целиком» здесь и в плюшечках, и в какашечках. Как всегда с паттернами и с архитектурой — выбирай, что для тебя сейчас важнее. То же — с простотой дизайна: она дает легкий старт и быструю разработку вначале, но ценой тяжелой поддержки и очень медленных итераций, когда проект разрастется.

Общие названия: Big Ball of Mud, Hello World, KISS + YAGNI.

Классика: Reactor [POSA2], Proactor [POSA2], Half-Sync/Half-Async [POSA2].

Бэкенд: Monolith.

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

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

Reactor (блокирующие вызовы)

(параллельные сценарии выделены цветом)

Один поток == одна задача. Каждый входящий запрос обрабатывается одним потоком, вызовы из кода наружу (в ОСь, сеть или библиотеки) блокирующие. Потоков-обработчиков можно наплодить много (Leader/Followers [POSA2] или Thread Pool (Master-Slave [POSA1])). Если обработчиков много, и они обращаются к общему состоянию — его нужно защищать мютексами. Код обработки простой, но большие проблемы, если нужно, чтобы один обработчик запроса мог повлиять на другие запросы, уже запущенные в выполнение. Многопоточный вариант недетерменистичен. Этот паттерн можно считать подходом по умолчанию для data flow систем (несложный бекенд). Многопоточный реактор — это шардинг (нарезка монолита — будет в следующей части статьи) на минималках.

Плюсы:

  • Простота кода сценариев.
  • Простота дебага.

Минусы (общие):

  • Нет мультикаста (нельзя сделать наружу несколько параллельных запросов).

Минусы (для однопоточного варианта):

  • Малая пропускная способность — только один сценарий в работе в любой момент времени.

Минусы (для многопоточного варианта):

  • Недетерминизм (нельзя проиграть события и воспроизвести баг).
  • Много потоков потребляют много ресурсов, и медленно переключаются.
  • Если есть изменяемое состояние — его надо защищать мютексами, что дает дополнительные задержки, и даже шанс словить дедлок.

Обычно на бекенде применяют многопоточный реактор. Но, например, для работы с железом (или как адаптер диска / базы) нужен однопоточный вариант — мы не можем направить в железо одновременно две команды на выполнение. Здесь (ре)актор блокируется, пока железо отработает первую команду и пришлет ответ на нее, и только потом актор просыпается и берет в работу следующий запрос из очереди сообщений.

Proactor (асинхронные вызовы)

Многозадачность без многопоточности. Один поток обрабатывает все события, вызовы наружу асинхронные (поэтому поток ни на чем не блокируется и всегда готов обслужить клиента). Так как мы не плодим потоки, то ниже потребление ресурсов, и в большинстве случаев (если не загрузить систему под завязку) получается мгновенная реакция на входящее сообщение. Можно даже держать поток-обработчик прогретым (busy waiting). Состояние не нужно защищать мютексами (не от кого, поток же один!). Так как все события входят последовательно, любой сценарий на любом шаге выполнения может изменить состояние актора, таким образом меняя поведение для всех последующих событий — то есть, конкурентно выполняемые сценарии легко могут влиять друг на друга. Выполнение детерминистично — если записать все входящие сообщения, и потом их воспроизвести — результат совпадет с изначальным. Цена за всю эту радость — код для каждого сценария разорван на асинхронные куски, поэтому сценарий целиком нельзя нормально прочесть или отдебажить. Подход применим для control flow, когда важно мгновенно изменить поведение всей системы, в том числе — уже запущенных сценариев, и не так важна пропускная способность (используется только одно ядро процессора). Второй конец палки: жертвуем простотой и мощностью ради гибкости.

Плюсы:

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

Минусы:

  • Очень неудобно читать и дебажить код сценариев, так как он разорван на кучу отдельных функций, которые вызываются внешними событиями — в коде нет привязки запроса к ответу.
  • Использует только одно ядро проца. Если хотим запустить в несколько потоков — теряем большинство плюсов.

Half-Sync/Half-Async (реактор на корутинах)

(серым обозначены интервалы, когда процессор обслуживает другой стек)

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

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

Плюсы:

  • Простота кода сценариев.
  • Простота дебага.
  • Относительно дешевое переключение контекста.
  • Относительно небольшие затраты ресурсов на выполняемый запрос.
  • Детерминизм (а значит — и воспроизводимость багов при проигрывании событий).
  • Возможность для сценариев влиять друг на друга (вероятно — вбросом исключения вместо ответа от блокирующего синхронного вызова).

Минусы:

  • Много служебного кода.
  • Сценарии могут отменять друг друга, но им довольно сложно согласованно изменять поведение, так как здесь у сценариев активный, а не реактивный, подход.
  • Использует только одно ядро проца. Если хотим запустить в несколько потоков — теряем детерминизм.
  • Таки сложно с мультикастом.
  • Не везде поддерживаются корутины (нужен хороший компилер и правильное железо).

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

Смесь

Часть вызовов наружу — быстрые синхронные (к флеш диску), часть — асинхронные (в сеть). Описан как оптимизация для уменьшения переключений контекста и упрощения кода. Где-то так же строится работа микросервисов (например, синхронно — с базой данных и асинхронно — с другими микросервисами). Еще это — очевидный вариант имплементации Content Enricher [EIP] или API Gateway [MP], когда мы получаем сообщение, добавляем в него данные из нашей базы (синхронное чтение) и переправляем обогащенное сообщение дальше кому-то более умному или ответственному (асинхронная посылка).

Состояние запроса

Если посмотреть, как хранятся состояния запросов у реактора и у вариантов проактора, получим следующую картинку:

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

Итого:

  • Мы посмотрели разницу между data flow и control flow
  • Познакомились с внутренностями акторов, использующих блокирующие и асинхронные операции ввода/вывода
  • Разобрали, где хранится состояние запросов, которыми актор сейчас занимается

Шарлотку приготовили — пора подумать, как ее нарезать. В следующей части статьи попробуем разрезать монолит messaging интерфейсами и смотреть на свойства получающихся систем.

Литература

[DDIA] Designing Data-Intensive Applications: The Big Ideas Behind Reliable, Scalable, and Maintainable Systems. Martin Kleppmann. O’Reilly Media, Inc. (2017).

[EIP] Enterprise Integration Patterns. Gregor Hohpe and Bobby Woolf. Addison-Wesley (2003).

[MP] Microservices Patterns: With Examples in Java. Chris Richardson. Manning Publications (2018).

[POSA1] Pattern-Oriented Software Architecture Volume 1: A System of Patterns. Frank Buschmann, Regine Meunier, Hans Rohnert, Peter Sommerlad and Michael Stal. John Wiley & Sons, Inc. (1996).

[POSA2] Pattern-Oriented Software Architecture Volume 2: Patterns for Concurrent and Networked Objects. Douglas C. Schmidt, Michael Stal, Hans Rohnert, Frank Buschmann. John Wiley & Sons, Inc. (2000).

Сподобалась стаття? Натискай «Подобається» внизу. Це допоможе автору виграти подарунок у програмі #ПишуНаDOU

👍ПодобаєтьсяСподобалось21
До обраногоВ обраному14
LinkedIn
Дозволені теги: blockquote, a, pre, code, ul, ol, li, b, i, del.
Ctrl + Enter
Дозволені теги: blockquote, a, pre, code, ul, ol, li, b, i, del.
Ctrl + Enter

День третий. Мы чем-то занимались два дня пора бы заняться формальными определениями того что нам кажется понятным и очевидным. А таких вещей много. Обычно я ввожу в ступор собеседников простым и вопросом буква — это объект или значение? Ответа не слышал. Так что займемся аксиоматикой. Приятно, когда вся терминология формализована. Фундаментальных понятий у нас три Concept< Token и Group (концепт, токен и группа).
Концепт-божественное понятие. Он формирует все и является прародителем всего сущего. Просто как-то это надо назвать вот и назвали. Можно было и объектом назвать, но это заезженное название и вызывает лишние ассоциации, от которых желательно избавится. Так как мы говорим о сущем, то это сущее обязано иметь размеры. За единицу размера принимаем токен-4 разряда. И понятие группа формирует порядок и принципы объединения концептов, ибо, не имея правил объединения мы на сможем создать ничего нового, из понятия токена. Согласно принятой концепции и токен и группа являются концептами. По причине того, что создавать и объяснять мы будем в текстовом виде будем обозначать объединение концептов (группы) скобками что не должно удивить не только программистов и математиков читающих эту белиберду, но и мало-мальски читающую публику. Объединение концептов имеет несколько важных аспектов:
1. Текстовое представление — для восприятия текста.
2. Метод хранения и доступа.
3. Семантический смысл.
4. Выполнение.
С первыми двумя почти очевидно или будет понятно по ходу дела, с третьим и четвертым аспектом не очень. Потому малость о семантическом смысле. Слушая кого-то, читая художественный текст или текст программы мы мысленно анализируем каждое слово или букву и подсознательно или сознательно выносим вердикт-истина/ложь каждой фразе или утверждению. Наш компьютер должен быть ничуть не хуже потому каждый концепт имеет метасвойство (|) выполнение. А концепты могут объединяться для выполнения не только как анализирует человек (по принципу И), но и по принципу ИЛИ.

Синтаксис. Хранение и доступ
Круглые «(» и «)» и фигурные «{» и «}» скобки представляют собой группы классов по типу хранения Collection, реализующих объединение любых концептов с последовательным доступом. Доступ к элементам Collection возможен как по индексу, так и по имени элемента группы. Отличие в синтаксисе в том, что элементы в круглых скобках разделяются запятыми. Выбор вида скобок в руках пользователя и наглядности применения.
Квадратные скобки «[» и «]» представляют концепт Array, реализующий объединение любых концептов с произвольным доступом (по индексу) и параллельным выполнением выполняемых концептов.
Выполнение

Результатом выполнения любого концепта является метасвойство (|) инициирующее одноименное Метасобытие перечислимого типа Boolean имеющего два значения «!» —Истина и «¬» -Ложь.
Успешное выполнение концепта — это выполнение со значением свойства «|» -Истина.
Каждый класс концептов имеет свое назначение и специфику выполнения.
Для успешного выполнения содержимого групп Collection необходимо успешное выполнение всех элементов группы (Выборка всех элементов группы. Семантическое И).
Концепты группы Array выполняется как выборка из Min элементов, имеющих успешное выполнение (семантическое Или) Max раз.
Выполнение групп концептов, порождаемых скобками Collection и Array зависит от типа подстрочных и надстрочных значений после закрывающей скобки.
Если это значения типа Token, то это свойства соответственно Min и Max которые определяют минимальное и максимальное значение успешных выполнений содержимого группы необходимых для успешного выполнения группы. По умолчанию Min=1, Max=0 что соответствует одному обязательному выполнению группы Collection и выборке одного элемента один раз группе Array.
Успешное выполнение группы заключается в успешном выполнении содержимого группы >=Min и <=Max раз. При значении Min=0 выполнение группы считается успешным даже если не было успешного выполнения ни одного элемента группы.

Перечислимые типы. События.
Определим группу Boolean с применением правила именованной скобки (Имя группы без пробела непосредственно после открывающей скобки).

  {Boolean Token {"!"  0   #True.» "¬“ 1 #False.»}}
Элементами этой группы являются два концепта класса Token с именами ! и ¬ и значениями соответственно 0 и 1. Если мы определим данные как тип Boolean, то можем использовать имена имен концептов этой группы как значения. Фактически мы получаем перечислимый тип, значениями которого являются имена концептов элементов группы хотя реально в переменной хранятся индексы элементов перечислимого множества.
Перечислимые типы имеют три вида значений:
1. Как концепты-элементы группы, или их значения.
2. Как индексы элемента группы.
3. Как имена концептов элементов группы.
4. Как события порожденные присваиванием соответствующего значения.
Boolean F !      #Определение переменной типа Boolean и присвоение значения Истина.» (прим. 1)
F:=0      #Ошибка присваивания хотя именно 0 значение.» 
F|  ¬    #Событие не истинности, за которым должна следовать подписка.»  Аналог оператора If:
F |{ ! a + b ¬ a-b} #Подписка на событие F. При истинности сумма a и b, и разность если F-ложь» 
  {Compare Token{ "="  0   #Равно»">"  1   #Больше.» "<"  2   #Меньше.» }}
Для перечислимого типа Compare именами значений служат знаки «=», «<» и «>»
В тексте программы событие выполнение «|» можно опустить, как и истинное значение события по умолчанию. Потому подписка по истинности выглядит вообще, как обычный текст, а событие | по невыполнению (ложь) это по смыслу привычное «Error»
Теперь мы можем сказать что такое буква. Это тип данных группы Alphabet а значение -имя соответствующего знака в группе Alphabet. Знаки являются объектами, индексы этого объекта в группе тоже как бы буквы)) Можно считать их и событиями данного типа данных.

Покурили. День второй божественной работы! У нас есть концепты и атрибуты. Давайте посмотрим что мы можем. Определяем концепт прямоугольник П с высотой h и шириной w
Rectangle П {Int h int w}.
Смотрим на работу. Шикарно. Но, ни о чем. Ну, можем обращаясь менять значения (изменяя состояние) , это не «оживляет» нашу конструкцию. Хорошо бы иметь события. Как бы понятно что изменение значений атрибутов должны иметь события «изменение значения» ( ну и еще какие-то). Допустим оно у нас есть и обозначается знаком «~» и куда б его прилепить, и когда оно появляется и как заставить чего-то совершить по возникновению этого события? Это синтаксически легко решается. Если за определением атрибута следует имя события, то за этим событием следует указание реакции на него (подписка). Если знака события нет, то и реакции нет. А теперь мы в можем создавать свои новые сущности «событие». Посмотрим как выглядит определение события Resize в нашем примере.
Rectangle П {Int h~ Resize int w~Resize Boolean Resize }
Поясним работу подробнее. Если при адресации к атрибутам h или w произошло изменение значения, то выполнится соответствующее событие, произойдет переход по подписке к некой сущности по имени Resize определенной почему-то как тип Boolean, но к нему тоже можем организовать подписки. Причем тут тип Boolean мы рассмотрим на следующий день. Будем определять перечислимые значения. А на сегодня отметим для четкого понимания. Наш суперкомпьютер имеет много шин и не проблема адресоваться (при необходимости) куда угодно. Если угодно! Подписки формируют последовательность выполнения. Ибо в общем случае подписок может быть много и событий много. И допускается параллельное выполнение. А вот порядок гарантирован в каждой ветке именно подписками. На досуге поразмышляйте на тему что такое алгоритм, и эквивалентна ли наша придуманная машина алгоритмам.
Есть еще тема для подумать. Момент возникновения события. Это не когда написали и не когда выполняется. Событие происходит в момент адресации!

изменение значений атрибутов должны иметь события
Событие происходит в момент адресации!

Вот тут уже противоречит, если адресация — это затягивание значения из памяти в регистр процессора. Поясню:

static int x = 2;

static void Foo(int* bar) {*bar = 2;}

Foo(&x);
Тут изменения значения переменной не произошло, а адресация была.

Устройство состоит из конечного числа пронумерованных ячеек, которые мы назовем концептами и абстрактного понятия «головка» функция которой адресоваться к концепту одним из видов адресации. Видов адресации 4. Для чтения, для записи, для выполнения и для инициализации. Каждый из видов адресации включает в себя составной частью просто адресацию. Это железная часть должна быть. Так что само собой. Адресация есть всегда, Изменения, только тогда, когда есть изменения.

Событие происходит в момент адресации!

Этим я уточнял момент возникновения всех событий.

Ок, а нафиг дергать подписчиков при адресации, если не произошло изменения? Кому такое интересно?

Ок, а нафиг дергать подписчиков при адресации, если не произошло изменения? Кому такое интересно?

Мало ли. Может кому-то счетчик понадобится. Разработчик должен дать инструмент. А кому и зачем это надо-решает пользователь. Только фраза не правильно построена. Кого ты называешь подписчиком? Я завтра с утра продолжу по поводу событий. И так до обеда занимался формулировками. Ты ж сам писал и в курсе сколько это времени отнимает. Вот когда все подробно опишу, тогда вернемся к обсуждению твоей статьи. Ибо если сейчас начну будет много не понятного. Пока еще раз прочитай что я написал. Лучше спрашивай что не понятно из изложенного. Кстати, насчет дерганий. Их не происходит. У нас супермашина и проверка на события не изменяет состояния. Может спокойно выполняться на многопроцессорных системах.

Может кому-то счетчик понадобится

Счетчик чего? Состояние системы не изменилось.

Кстати, насчет дерганий. Их не происходит. У нас супермашина и проверка на события не изменяет состояния.

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

Обычно как раз наоборот делают: в аксессоре прописывают нотификацию только если было реальное изменение:

class Foo {
public:
    Foo& operator=(const Foo& other) {
        if(value_ == other.value_) return;
        value_ = other.value_;
        NotifySubscribers();
    }
    void Subscribe(Subscriber& sub) {subscribers_.push_back(s);}

private:
    void NotifySubscribers() {for(Subscriber& s : subscribers_) s.Notify(*this);}
    
    list<Subscriber&> subscribers_;
    int value_;
};
Я говорю, что подписчику на изменение состояния неинтересны события, состояние не меняющие.

А с чего ты решаешь за пользователя? Сколько угодно случаев когда надо запустить что-то при любой адресации. Не важно для чтения или для записи. Или какой-то анализ сделать. Вау, а пример твой для классической машины)) Там действительно занимает и время и память. В правильной машине это делает машина. Только лови события подпиской. Определение целого i:
Int i~*** и вместо звездочек пиши что надо выполнить по событию изменение значения. А ниже по событию «Адресация»
Int i:***

Сколько угодно случаев когда надо запустить что-то при любой адресации. Не важно для чтения или для записи. Или какой-то анализ сделать.

Не верю в «сколько угодно». Хотелось бы штук 5 примеров.

Int i~*** и вместо звездочек пиши что надо выполнить по событию изменение значения.

А для прямоугольника нельзя подписаться на изменение значения площади. Потому что площадь он не хранит в переменной. Вот если прямоугольник повернули на 90* — площадь не изменилась, и нотификации не должно быть. А в старом С++ спокойно такая подписка на площадь делается.

А для прямоугольника нельзя подписаться на изменение значения площади.

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

Написано хорошо. Эмоционально и визуально. И даже работает. Собственно, ничего удивительного. Умные люди делали. Только я не нашел определений событий, акторов, Data flow и Control flow. Пример фотика и затем объявление

Это был data flow

сложно считать определением. Со многим согласен ибо приведены объективно существующие вещи и проблемы, но хотел бы навести порядок. Подход обобщения «сверху» в общем правильный, только базовые вещи не проработаны и не осознаны и потому с необходимостью экспоненциально будут расти сложности и появление новых и новых сущностей. Замечаний и неясностей очень много потому буду по частям и сколько смогу. Дальше будет видно и по времени и по наличию интереса.
Итак, по условиям задачи у нас есть суперкомпьютер с кучей процессоров, шин и памятью. И нам надо на нем реализовать кучу чего-то сложного как можно проще. Надеюсь, условия задачи поставлено правильно. Давайте определять с нуля. Вот ничего нет и нам все что надо надо определить. Все что не надо определять не будем!) Воистину божественная работа!
Итак, нам надо с чем-то работать. Оно будет иметь имя (надо же как-то с ним обращаться) и Help (на всякий случай. Не надо программисту объяснять что это за случаи). Классы вот этого что мы придумываем назовем «концепт». Само собой что-то еще должно быть и что-то обозначать. Ясный пень это «что-то» должно содержаться в памяти и иметь какие-то значения (назовем это «что-то» атрибутами) Опять же не программистам рассказывать за типы данных и имена этих значений. На именах я б остановился, ибо широко известная поговорка «Как корабль вы назовете, так он и поплывет» имеет сакральный смысл. Это не только способ обращаться к этому чему-то , но и является носителем семантического содержания. Ибо определяет терминологию общения при работе с этим концептом.
Хвалим себя за проделанную работу и идем курить. Заодно продумаем что мы сотворили.

Только я не нашел определений событий, акторов, Data flow и Control flow.

Акторов начинали смотреть тут dou.ua/forums/topic/36197 и как неточное определение (простыми словами для обычных людей) оттуда: Актор — это сущность с определенным поведением (код) и состоянием (данные), которая взаимодействует с другими подобными сущностями (акторами) только асинхронной посылкой сообщений. В любой конкретный момент актор или ожидает входящего сообщения (спит), или обрабатывает одно из сообщений. Обычно входящие сообщения складываются в очередь (мейлбокс), а поток-обработчик их оттуда по одному вынимает и процессит. Наверное, знакомо.

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

Событие не определено, так как является довольно базовым философским понятием. Можно попытаться определить, как «наблюдаемое в момент времени изменение свойства». Но теперь нам надо поопределять уже 5 слов из этого определения(((

с необходимостью экспоненциально будут расти сложности и появление новых и новых сущностей

Наоборот, что и показывает следующая статья. Сверхдетализация приводит к:
1) необходимости постоянных поправок
2) неприменимости модели к изменчивому и многоформенному миру
поэтому разум манипулирует идеями — достаточно абстрагированными сущностями, чтобы для каждого конкретного осознанного предмета не нужно было пересоздавать идею. Собак дофига, а малой уже каждую конкретную из них тыкает пальцем и говорит «собака». Вот на этом уровне и удобно оставаться, чтобы не нагородить экспоненциальной сложности (потеряв лес в стопке дров).

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

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

(д) Данные проходят кучу этапов, как в мясорубке, но сама схема не меняется.

За мясорубку позже напишу. Как и за

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

Мы о разном. Фундамент должен быть един.

Хорошая классификация.
Ее можно на английский и на dev.to или medium!
Там бОльшая часть технического материала — куда слабее, чуть пожует автор оф. доку и опа — себе пиар в виде статьи :)

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

Молодец, хороший Бот.
По крайней мере нос по ветру держишь четко !

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

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

А ее в принципе нет. Сложную задачу нельзя решить просто. Если у тебя 10 томов требований бизнеса по 300 страниц — их нельзя описать 1000 строк кода.

Архитектуры для систем акторов не попадаются в литературе

Бывает, попадаются при описании парадигм Reactive Programming и Agent-Oriented Programming. У того же Вернона, например, есть книга Reactive Messaging Patterns With the Actor Model, там раскрываются некоторые аспекты таких архитектур.

Я ее смотрел. Там что-то вроде повторения Enterprise Integration Patterns. То есть, набор советов и дизайн паттернов для создания каналов и пробросов сообщений. Но не видно, как это все сложить в кучу, чтобы построить что-то с нуля. То есть — чемоданчик с инструментами, а не чертеж того, для чего этих инструментов в чемодан отсыпали.

Разница между архитектурными и дизайн паттернами как раз в уровне применимости. Архитектурные — это что мы будем делать. То есть — как нарисовать нашу систему. Слои, гексагоналка, трубопровод. Это распределенный шардингом монолит, микросервисы, или эрлангоподобные наносервисы. Модель многопоточности. База общая или раздельные. Фронтэнд общий или раздельные. То есть — из каких кусков состоит система и как эти куски должны взаимодействовать.

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

Ну и снова вброшу martinfowler.com/...​oservice-verdict/path.png
Если бы с архитектурой было все очевидно — эта картинка бы не существовала.

Ну да, но системы такого рода часто создаются ad hoc для каждого конкретного заказчика/проекта, со всеми условностями такой разработки. Выжать из этого потом что-то в абстрактный architectural guide наверняка не так просто.

Для меня лично одним из первых полезных примеров архитектуры такого рода был старый Win32 GDI API. Вот там, как ни странно, была реализована практически настоящая (и неплохо работающая) актор-система с объектами, мэйлбоксами, синхронной/асинхронной коммуникацией через SendMessage/PostMessage и прочим. Она была и легко доступна для изучения/опытов, и хорошо документирована и идеологически стройна.

Выжать из этого потом что-то в абстрактный architectural guide наверняка не так просто.

Надеюсь, что возможно. Есть правило: не резать асинхронным интерфейсом клубок связной логики. По сути — о нем и картинка Фаулера. Народ разбивает домен на микросервисы, не понимая до конца требований, то есть — связности. И огребает смерть проекта по техническим причинам.

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

Паттернов уже известно порядка двух тысяч (кто-то их коллекционирует). Too much information is no information. Когда было 50 паттернов — их можно было знать, понимать, и это было полезно. Сейчас — нет. Надо видеть принципы, лежащие под паттернами. Надеюсь передать то, что там нашел.

Выжать из этого потом что-то в абстрактный architectural guide наверняка не так просто.
Надеюсь, что возможно

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

Если этого нет, то тогда — лингвистическое и филологическое.
Вот есть алфавит — как соотносятся в нем буквы К и Ю?

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

в каких плоскостях монолит (домен) режется асинхронными интерфейсами.

или невозможно, или бессмысленно

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

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

В чистом виде конечно ни первое ни второе не встречается.
Но подходы эти — весьма разные:
Анализ структуры здания и анализ вида стихосложения

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

то есть уход в микросервисы, это еще бОльший уход от инженерной практики анализа и проектирования. только теперь сами сервисы выступают такими элементами языка. Простыми ведь, верно? Имена пользователей хранятся и обратабатываются одной группой сервисов, их адреса — другой, а баланс на счете — третьей
Хотя это ж должен был быть один объект, с полем имя, коллекцией адресов, и вычисляемым полем — денег в кошельке.
А его нигде в системе не существует ни в какой момент времени. Даже при отображении пользователю — «фронтенд» запросит его куски, и как-то покажет. причем в одном отображении так, в другом сяк.

DDD, насколько я знаю — не предлагает никаких средств такого управляемого распила цельной доменной сущности. Он же из времен — тотальной победы ООПроектирования, 90ых, начала 2000ых
Он предлагает создавать — иероглифическое письмо — один «символ»-одно «слово». причем не одно, а для каждого bounded context свое :)

И вот при увеличении нагрузки этот самый инженерный анализ сталкивается с реальной жизнью. Где-то не хватает скорости базы, и фиг ее промасштабируешь — да потому что домен целиком не шардится. Где-то надо быстро дать ответ, а чтобы собрать его — надо обойти пол-домена, нашпигованного мютексами. Где-то логики не много, но бизнесу важно 24/7 работоспособность. А где-то аналитика пилит базу по неделе на запрос. И вот мы все эти -ilities пытаемся запихать в одно приложение. А они его разрывают каждая в свою сторону.

Interestingly, software architecture has very little to do with functional requirements. You can implement a set of use cases — an application’s functional requirements — with any architecture. In fact, it’s common for successful applications to be big balls of mud. Architecture matters, however, because of how it affects the so-called quality of service requirements, also called nonfunctional requirements, quality attributes, or ilities. [MP]

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

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

Итого: у нас нефункциональное требование, которого не было ни в спецификации задач, ни в инженерном анализе этой спецификации, привело к возникновению «ниоткуда» компонента системы со свойствами, сильно отличными от той логики, которую описывал пользователь.

И вот при увеличении нагрузки этот самый инженерный анализ сталкивается с реальной жизнью

думаю поэтому подъем уровня абстракций в языках программирования остановился.
абстракции — текут. и если ЯП лишен возможности бороться с протечками, ну фсё, меняйте ЯП :)

привело к возникновению «ниоткуда» компонента системы со свойствами, сильно отличными от той логики, которую описывал пользователь.

именно.
как недавно я писал и превращается 1 предложение в 1000 строк кода.
и спека с тз — всего не описывает. в ней всего-то 100 строк
а вот остальные 900 — берутся «ниоткуда». И чтобы оттуда взять их правильные — и нужна квалификация программиста, что берет их из «ниоткуда»

Ну а ИИ «конечно» справится с этим лучше :D

Спасибо большое, очень интересный материал !

Возьмём фотик и моГилку, дальше с серьезным лицом читать было сложно)
Денис, привет )

Возьмем фотик и могилку.

 не надо исправлять ))

Підписатись на коментарі