fk0, легенда (22.10.2019 11:16, просмотров: 1490) ответил =AlexD= на Не понимаю я вот этого противопоставления потоки vs КА. КА внутри потоков работающие строго по блокируемым системным вызовам - наш путь, не?
Да, примерно об этом я и думаю. Что систему КА можно запускать параллельно, на пуле потоков, по выбирая готовые к запуску по мере наличия интересующих конкретные КА событий (это важный момент, т.к. планировщик избавляет от важной проблемы Big Loop'а -- большой латентности из-за чрезмерно долгого оборачивания цикла).
КА при этом обладают своим мьютексом, могут взаимодействовать через систему событий и планировщик, могут напрямую вызывать другие КА, но строго в одну сторону: есть чёткое разделение на модули/классы/автоматы используемые каким-либо другим модулем, и использующие какой-либо другой модуль. Прямой вызов возможен только в сторону используемого модуля, и говорится, что использующий модуль знает о свойствах используемого. В обратную сторону вызов запрещён, включая callbacks -- следует использовать событийную систему для передачи информации в обратную сторону (поверх событийной системы могут быть реализованы контейнеры вроде однонаправленных очередей сообщений, fifo-буферов, Go-каналов, recv-reply "шлюзов" из QNX и т.п.) И используемый модуль не знает ничего об использующих его модулях. Т.е. все зависимости ориентированы строго, условного говоря, сверху вниз, в одну сторону. Это позволяет избежать взаимоблокировок (dead locks) и рекурсий.
Выстраивать архитектуру системы нужно с деления на модули (классы, автоматы) и создания диаграммы связей между ними. Потом внутри модулей появляются автоматы (в простейшем случае, модуль -- это автомат). Сами автоматы могут задаваться в упрощённом виде, в виде "сопрограмм", представляемых в виде функций на C (т.е. когда состояние определяется счётчиком программных инструкций и значениями переменных). Это с одной стороны плохой подход, но он увы, ускоряет программирование. Кроме того, верификацию программ в системах типа Promela удобнее выполнять тоже при таком подходе.
Суть же "сопрограмм" в том, что они представляют обычные функций, у которых стек разделён как бы на две части: основная часть, на которую указывает регистр FP (и SP в момент входа в функцию) располагается не в стеке, а в блоке памяти выделенном именно для данного экземпляра (вызова) сопрограммы в куче. Вторая часть стека, куда искусственно переносится регистр SP -- в стеке потока, который исполняет сопрограмму в текущий момент. И эта часть стека "исчезает" при любой блокировке сопрограммы. Таким образом, сопрограммы могут вызывать обычные функций, но не могут блокироваться в последних. Могут вызывать другие сопрограммы и могут в них блокироваться. Вызов сопрограммы -- относительно дорогая операция (нужно выделение памяти), а вызов обычной функции -- дешёвая. Сопрограмма сохраняет на момент блокировки все свои локальные переменные, но не может использовать массивы с переменным числом аргументов или функцию alloca (т.к. смещение SP достигается через вызов alloca с рассчитанным значением аргумента). Минимально на каждый экземпляр, на каждую вызванную как отдельную "задачу" сопрограмму, нужен объём памяти стека достаточный для хранения её локальных переменных и обработки прерываний. То есть система начинает иметь смысл только наличии системы прерываний обрабатываемых на отдельном стеке, когда на стеке прерванной задачи не используется более нескольких слов. Последнее технически реализуемо на почти любых архитектурах, если переписывать обработчики прерываний на ассемблере. Но в типичных "рантаймах" обычно подразумевается общий стек, что является препятствием. На ряде архитектур с аппаратным стеком (PIC18) такой подход невозможен, как и многопоточность.
[ZX]