Большинство дизайнерских решений, принятых во время разработки этой библиотеки, являются результатом следующих требований.
Подъем. Statechart должен...
быть полностью безопасным. По возможности, несоответствия типов должны быть отмечены ошибкой во время компиляции.
Не требует использования генератора кода. Многие из существующих FSM-решений вынуждают разработчика проектировать государственную машину либо графически, либо на специализированном языке. Все или часть кода генерируется
позволяет легко трансформировать UML-статус (определяется вhttp://www.omg.org/cgi-bin/doc?formal/03-03-01)в рабочую машину. Напротив, существующая реализация на C++ машины состояний должна быть довольно тривиальной для преобразования в UML. В частности, следует поддерживать следующие функции государственных машин:
Иерархические (составные, вложенные) состояния
Ортогональные (конкурентные) состояния
Вступление, выход и переход
Охрана
Неглубокая/глубокая история
произвести настраиваемую реакцию, когда исключение C++ распространяется из кода пользователя
поддерживают синхронные и асинхронные машины состояния и оставляют его пользователю, в потоке которого будет работать машина асинхронного состояния. Пользователи также должны иметь возможность использовать библиотеку потоков по своему выбору.
поддержка разработки произвольно больших и сложных государственных машин. Несколько разработчиков смогут работать на одной и той же машине одновременно
позволяет пользователю настроить все управление ресурсами так, чтобы библиотека могла использоваться для приложений с жесткими требованиями реального времени.
как можно больше времени на сборку. В частности, недействительные государственные машины не должны компилироваться.
предлагает разумную производительность для широкого спектра приложений
Прежде чем я начал развивать эту библиотеку, я посмотрел на следующие рамки:
Фреймворк, сопровождающий книгу «Практические государственные диаграммы на C/C++» Миро Самека, CMP Books, ISBN: 1-57820-110-1 http://www.quantum-leaps.com Не удовлетворяет по меньшей мере требованиям 1, 3, 4, 6, 8.
Фреймворк, сопровождающий «Rhapsody in C++» от ILogix (решение для генератора кода) http://www.ilogix.com/sublevel.aspx?id=53 Это похоже на сравнение яблок с апельсинами. Тем не менее, нет никакой врожденной причины, по которой генератор кода не может производить код, который может быть легко понят и изменен людьми. Не удовлетворяет по крайней мере требованиям 2, 4, 5, 6, 8 (до генерации кода достаточно проверки ошибок).
Рамки, сопровождающие статью «Государственный машинный дизайн на C++» http://www.ddj.com/184401236?pgno=1 Не удовлетворяет по меньшей мере требованиям 1, 3, 4, 5 (нет прямой поддержки потоков), 6, 8.
Я верю, что Буст. Statechart удовлетворяет всем требованиям.
Эта еще не широко известная особенность государственного аппарата обусловлена тем, что каждое состояние представлено классом. При входе в состояние создается объект класса, и объект позже разрушается, когда государственная машина выходит из состояния. Любые данные, которые полезны только до тех пор, пока машина находится в состоянии, могут (и должны) быть, таким образом, членом государства. Эта функция в сочетании с возможностью распространения государственной машины над несколькими переводными единицами позволяет практически неограниченную масштабируемость.
В большинстве существующих фреймворков FSM вся государственная машина работает в одной среде (контекст). То есть все ручки ресурсов и переменные, локальные для государственной машины, хранятся в одном месте (как правило, как члены класса, который также происходит от какого-то класса государственной машинной базы). Для больших государственных машин это часто приводит к тому, что класс имеет огромное количество членов данных, большинство из которых необходимы лишь ненадолго в крошечной части машины. Таким образом, государственный машинный класс часто становится горячей точкой изменения, что приводит к частым рекомпиляциям всей государственной машины.
Пункт FAQ «Что так круто в отношении локального хранилища?» далее объясняет это, сравнивая учебник StopWatch с поведенчески эквивалентной версией, которая не использует государственное локальное хранилище.
Фреймворк машины состояния поддерживает динамическую настраиваемость, если вся компоновка машины состояния может быть определена во время выполнения («выкладка» относится к состояниям и переходам, действия по-прежнему задаются обычным кодом C++). То есть данные, доступные только во время выполнения, могут быть использованы для создания произвольно больших машин. См. "A Multiple Substring Search Algorithm" by Moishe Halibard and Moshe Rubin in June 2002 issue of CUJ for a good example (unfortunately not available online).
С другой стороны, это структуры государственных машин, которые требуют, чтобы макет был указан во время компиляции.
Государственные машины, построенные в режиме бега, почти всегда убегают с простой государственной моделью (ни иерархических состояний, ни ортогональных состояний, ни действий входа и выхода, ни истории), потому что компоновка очень часто совместима с алгоритмом. С другой стороны, машинные макеты, которые фиксируются во время компиляции, почти всегда разрабатываются людьми, которые часто нуждаются/желают сложной модели состояния, чтобы сохранить сложность на приемлемых уровнях. Таким образом, динамически настраиваемые фреймворки FSM часто оптимизированы для простых плоских машин, в то время как воплощения статического варианта, как правило, предлагают больше возможностей для абстракции.
Тем не менее, полнофункциональные динамические библиотеки FSM существуют. Итак, вопрос:
Why not use a dynamically configurable FSM library for all state
machines?
Можно утверждать, что динамично настраиваемая структура FSM - это все, что когда-либо нужно, потому что любая государственная машина может быть реализована с ней. Однако из-за своей природы такой каркас имеет ряд недостатков при использовании для реализации статических машин:
Оптимизации и проверки времени компиляции не могут быть сделаны. Например, Boost. Statechart определяетнаиболее распространенный контекстсостояния перехода и назначения во время компиляции. Кроме того, проверка времени компиляции гарантирует, что машина состояния является действительной (например, что нет переходов между ортогональными состояниями).
Двойная отправка неизбежно должна осуществляться с помощью какой-то таблицы. Как утверждается вДвойной диспетчерской, это масштабируется плохо.
Чтобы обеспечить быстрый поиск стола, состояния и события должны быть представлены целым числом. Чтобы таблица была как можно меньше, нумерация должна быть непрерывной, например, если есть десять состояний, лучше всего использовать идентификаторы 0-9. Для обеспечения непрерывности идентификаторов все состояния лучше всего определять в одном и том же файле заголовка. То же самое касается и событий. Опять же, это не масштабируется.
Поскольку события, несущие параметры, не представлены типом, необходимо использовать некоторое общее событие с картой свойств, и безопасность типа обеспечивается во время выполнения, а не во время компиляции.
Именно по этим причинам, это и есть. Statechart был построен с нуля до не поддержки динамической конфигурации. Однако это не означает, что невозможно динамически формировать машину, реализованную с этой библиотекой. Например, охранники могут быть использованы для осуществления различных переходов в зависимости от входных данных только в рабочее время. Однако такие изменения макета всегда будут ограничиваться тем, что можно предвидеть до компиляции. Несколько связанная библиотека, платформа для бустера::spirit parser, позволяет примерно такую же настройку времени выполнения.
Нет ни одного слова об обработке ошибок в спецификациях семантики машины состояния UML. Более того, большинство существующих FSM-решений также, похоже, игнорируют эту проблему.
Why an FSM library should support error handling
Рассмотрим следующую конфигурацию состояния:
Оба состояния определяют действия ввода (x() и y()). Всякий раз, когда состояние A становится активным, вызов x() будет немедленно сопровождаться вызовом y(). y() может зависеть от побочных эффектов x(). Таким образом, выполнение y() не имеет смысла, если x() терпит неудачу. Это не эзотерический угловой случай, но происходит каждый день в государственных машинах все время. Например, x() может приобрести память, содержимое которой позже изменено y(). Существует иная, но с точки зрения обработки ошибок одинаково критическая ситуация в Тюториальной области под Установить государственную информацию из машины, когда Running::~Running() имеет доступ к внешнему состоянию Active. Если бы действие входа Active потерпело неудачу и Running было введено в любом случае, то Running действия выхода ссылались бы на неопределенное поведение. Ситуация обработки ошибок с внешними и внутренними состояниями напоминает ту, которая имеет базовые и производные классы: Если конструктор базового класса терпит неудачу (заявив исключение), строительство прерывается, производный конструктор класса не называется и объект никогда не приходит к жизни. В большинстве традиционных фреймворков FSM такая ситуация с ошибками относительно проста в решении , если ошибка может быть распространена на клиента государственной машины. В этом случае неудачное действие просто распространяет исключение C++ в рамки. Рамки обычно не захватывают исключение, чтобы клиент государственной машины мог справиться с ним. Обратите внимание, что после этого клиент больше не может использовать объект государственной машины, потому что он находится либо в неизвестном состоянии, либо каркас уже сбросил состояние из-за исключения (например, с защитником сферы охвата). То есть, по своей природе, государственные машины обычно предлагают только основную безопасность исключения. Тем не менее, обработка ошибок с традиционными фреймворками FSM становится удивительно громоздкой, как только многие действия могут потерпеть неудачу, а государственная машина сама должна изящно справляться с этими ошибками. Обычно, неудачное действие (например, x()) затем публикует соответствующее событие ошибки и устанавливает глобальную переменную ошибок к истине. Каждое последующее действие (например, y()) сначала должно проверить переменную ошибки, прежде чем делать что-либо. После того, как все действия завершены (не делая ничего!), ранее опубликованное событие ошибки должно быть обработано, что приводит к исполнению действия средства правовой защиты. Пожалуйста, обратите внимание, что недостаточно просто очертить событие ошибки, поскольку другие события все еще могут быть отложены. Вместо этого событие ошибки имеет абсолютный приоритет и должно быть рассмотрено немедленно. Есть несколько менее громоздкие подходы к обработке ошибок FSM, но они обычно требуют изменения схемы государственного устройства и, таким образом, затеняют нормальное поведение. Независимо от того, какой подход используется, программисты обычно вынуждены писать много кода, который имеет дело с ошибками, и большая часть этого кода не посвящена обработке ошибок, но распространению ошибок.
Error handling support in Boost.Statechart
Исключения C++ могут распространяться из любого действия, чтобы сигнализировать о провале. В зависимости от того, как настраивается государственная машина, такое исключение немедленно распространяется на клиента государственной машины или вылавливается и превращается в специальное событие, которое отправляется немедленно. Более подробную информацию см. в главе Исполнение в учебнике.
Two stage exit
Действие выхода может быть реализовано путем добавления деструктора в состояние. Из-за природы деструкторов есть два недостатка в этом подходе:
Поскольку деструкторы C++ практически никогда не должны бросать, нельзя просто распространять исключение из действия выхода, как это происходит, когда одно из других действий не удается.
Когда объектstate_machine<>разрушается, то неизбежно разрушаются и все текущие активные состояния. То есть терминация государственной машины связана с уничтожением государственного машинного объекта.
По моему опыту, ни один из вышеперечисленных пунктов обычно не является проблемой на практике, так как...
Действия выхода часто не могут потерпеть неудачу. Если они могут, то такая неудача обычно либо
не представляет интереса для внешнего мира, т.е. неудача может быть просто проигнорирована.
Это настолько серьезно, что заявка должна быть прекращена в любом случае. В такой ситуации раскручивание стопки почти никогда не желательно, и сбой лучше сигнализируется через другие механизмы (например, аборт).
Для правильной очистки часто действия выходадолжны выполняться при разрушении объекта государственной машины, даже если он разрушается в результате размотки стопки.
Тем не менее, некоторые люди выдвинули теоретические аргументы и сценарии реального мира, которые показывают, что действие выхода для картирования деструктора может быть проблемой и что обходные пути слишком громоздки. Вот почему 2 ступенчатый выход теперь поддерживается.
Для асинхронных государственных машин разные приложения имеют довольно разнообразные требования:
В некоторых приложениях каждая государственная машина должна работать в своей собственной нити, другие приложения являются однопоточными и запускают все машины в одной нити.
Для некоторых приложений идеально подходит планировщик FIFO, другим нужны приоритетные или EDF-планировщики.
Для некоторых приложений поддержка::thread-библиотека просто прекрасна, другие могут захотеть использовать другую библиотеку потоков, но другие приложения работают на платформах без ОС, где ISR являются единственным режимом одновременного выполнения.
Out of the box behavior
По умолчанию asynchronous_state_machine<> объекты подтипа обслуживаются объектом fifo_tabler<>. fifo_tabler<> не запирается или не ждет в однонаправленных приложениях и использует примитивы для повышения скорости::thread для выполнения этого в многоадресных программах. Более того, объект fifo_tabler<> может обслуживать произвольное число объектов подтипа asynchronous_state_machine<>. Под капотом fifo_tabler<> является лишь тонкой оберткой вокруг объекта FifoWorker параметр шаблона (который управляет очередью и обеспечивает безопасность потока) и процессор_контейнер<> (который управляет сроком службы государственных машин).
Стандарт UML требует, чтобы событие, не вызвавшее реакцию в государственной машине, было молча отброшено. Поскольку объект fifo_tabler<> сам по себе является государственной машиной, события, предназначенные для того, чтобы больше не существовало объектов asynchronous_state_machine<>, также бесшумно отбрасываются. Это подтверждается тем, что объекты подтипа asynchronous_state_machine<> не могут быть построены или уничтожены непосредственно. Вместо этого, это должно быть сделано через fifo_tabler<>::Создать_процессор<>() и fifo_tabler<>::destroy_processor() (процессор означает, что fifo_tabler<> может разместить только event_processor<>> подтип объектов; asynchronous_state_machine<> является лишь одним из способов реализации такого процессора. Более того, Создать_процессор<>() возвращает только объект процессор_handle. Отныне это должно быть использовано для инициирования, очередей для, прекращения и уничтожения государственной машины через планировщик.
Customization
Если пользователь должен настроить поведение планировщика, она может сделать это, мгновенный fifo_tabler<> с ее собственным классом моделирования концепции FifoWorker. Я рассматривал гораздо более общий дизайн, где блокировка и ожидание реализуются в политике, но я до сих пор не придумал чистый и простой интерфейс для него. Особенно ожидание немного сложно смоделировать, поскольку некоторые платформы имеют переменные состояния, другие имеют события, и все же другие не имеют никакого представления о ожидании чего-либо (они вместо этого петируют, пока не придет новое событие, предположительно через ISR). Учитывая относительно несколько строк кода, необходимых для реализации пользовательского FifoWorker Тип и тот факт, что практически все приложения будут реализовывать как минимум один такой класс, в любом случае это не кажется целесообразным. Приложения, требующие менее или более сложного управления временем процессора событий, могут настроить поведение на более грубом уровне, используя пользовательский тип Scheduler. В настоящее время это относится также и к заявкам, требующим планов выдачи пропусков, не относящихся к ФИФО. Тем не менее, наслаждение. Statechart, вероятно, предоставит Приоритет_планировщик в будущем, чтобы пользовательские планировщики должны быть реализованы только в редких случаях.
Все пользовательские функции (react функции-члены, входные, выходные и переходные действия) должны быть членами класса. Это объясняется следующими причинами:
Концепция государственного местного хранения требует, чтобы действия по вхождению в государство и выходу из него осуществлялись в качестве членов.
reactФункции членов и переходные действия часто получают доступ к данным о состоянии на местном уровне. Таким образом, наиболее естественно реализовать эти функции как члены класса, данные о которых функции будут работать в любом случае
Точки соединения UML не поддерживаются, потому что произвольно сложные охранные выражения могут быть легко реализованы с помощью custom_reaction>.
Dynamic choice points
В настоящее время нет прямой поддержки этого элемента UML, потому что его поведение часто может быть реализовано с помощью custom_reaction>. В редких случаях это невозможно, а именно когда точка выбора оказывается исходным состоянием. Затем поведение может быть легко реализовано следующим образом:
choice_point<> в настоящее время не является частью Boost. Statechart, главным образом потому, что Я боюсь, что новички могут использовать его в местах, где им будет лучше с custom_reaction<>. Если спрос достаточно высок, я добавлю его в библиотеку.
Deep history of orthogonal regions
Глубокая история государств с ортогональными регионами в настоящее время не поддерживается:
Попытки реализовать эту государственную диаграмму приведут к ошибке в компиляционном времени, потому что B имеет ортогональные области, а его прямое или косвенное внешнее состояние содержит глубинную историю псевдогосударства. Другими словами, государство, содержащее глубокую историю, псевдогосударство не должно иметь прямых или косвенных внутренних состояний, которые сами имеют ортогональные области. Это ограничение связано с тем, что полная глубокая поддержка истории будет более сложной для реализации и будет потреблять больше ресурсов, чем осуществляемая в настоящее время ограниченная поддержка глубокой истории. Более того, полное глубокое поведение может быть легко реализовано с мелкой историей:
Конечно, это работает только в том случае, если C, D, E или любое из их прямых или косвенных внутренних состояний не имеют ортогональных областей. Если нет, то этот шаблон должен применяться рекурсивно.
Synchronization (join and fork) bars
Синхронизация баров не поддерживается, то есть переход всегда происходит в одном состоянии и всегда заканчивается ровно в одном состоянии. Присоединяйтесь к барам иногда полезно, но их поведение можно легко подражать охранникам. Поддержка вилочных баров сделает реализацию much более сложной, и они нужны только редко.
Event dispatch to orthogonal regions
Погром. Алгоритм отправки событий Statechart отличается от того, который указан в оригинале статьи David Harel и в стандарте UML. Оба мандата предусматривают, что каждое мероприятие направляется во все ортогональные области государственной машины. Пример:
Здесь алгоритм отправки Harel/UML указывает, что машина должна перейти от (B,D) к (C,E) при обработке события EvX. Из-за тонкостей, которые Харель описывает в главе 7 своей статьи , реализация этого алгоритма не только довольно сложна, но и намного медленнее, чем упрощенная версия, используемая Boost. Statechart, который прекращает поиск реакции, как только он нашел подходящий для текущего события. То есть, если бы этот пример был реализован с этой библиотекой, машина не детерминистически переходила бы от (B,D) к (C,D) или (B,E). Эта версия была выбрана потому, что, по моему опыту, в реальных машинах разные ортогональные области часто не уточняют переходы для тех же событий. Для редких случаев, когда они это делают, поведение UML может быть легко эмулировано следующим образом:
Transitions across orthogonal regions
Переходы по ортогональным областям в настоящее время отмечены ошибкой во время компиляции (спецификации UML явно позволяют им, в то время как Harel не упоминает их вообще). Я решил не поддерживать их, потому что я ошибочно пытался осуществить такой переход несколько раз, но никогда не сталкивался с ситуацией, когда это имело бы смысл. Если вам нужно сделать такие переходы, пожалуйста, дайте мне знать!
Статья The Boost Statechart Library - Rationale раздела может быть полезна для разработчиков на c++ и boost.
Материалы статей собраны из открытых источников, владелец сайта не претендует на авторство. Там где авторство установить не удалось, материал подаётся без имени автора. В случае если Вы считаете, что Ваши права нарушены, пожалуйста, свяжитесь с владельцем сайта.