Страницы

Поиск по вопросам

воскресенье, 15 декабря 2019 г.

Параллельные состояния в QStateMachine

#cpp #qt #qt5 #qt_faq


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

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


Ответы

Ответ 1



Реализация параллельных состояний в Qt State Machine Framework в отличие от последовательных имеет одну существенную особенность. Но для полноты картины начнём именно с последовательных. Создадим машину и пару состояний. QStateMachine *machine = new QStateMachine(); QState *state1 = new QState(machine); QState *state2 = new QState(machine); Если для нашей машины предполагается, что она, при наличии определённых условий (скажем, при прохождении двух ранее объявленных состояний), должна завершить свою работу, то удобно добавить в неё состояние завершения работы. QFinalState *final_state = new QFinalState(machine); Использование QFinalState удобно тем, что при входе в него контекста выполнения этот тип состояния автоматически отправляет своему родителю сигнал finished(). Если родителем является объект QStateMachine, то машина будет остановлена, если же это объект QState, то уже тот в свою очередь отправит такой же сигнал вверх по цепочке. Эта особенность в полной мере используется и при параллельном подключении состояний, что будет рассмотрено ниже. К слову сказать, во всех примерах Qt-справки машина имеет неопределённую длительность жизни, тогда как на практике часто встречаются задачи, которые, с одной стороны требуют асинхронного выполнения (например, отправка и обработка сетевых запросов), а с другой - возникают по мере необходимости. Нет никакого интереса содержать в памяти объект машины со всеми состояниями, если, скажем, надо что-то выполнять регулярно, но не постоянно. Для таких случаев при объявлении машины можно просто подключить соответствующие сигналы и объект машины ликвидирует сам себя и все дочерние объекты, как только завершит работу. connect(machine, &QStateMachine::stopped, machine, &QStateMachine::deleteLater); connect(machine, &QStateMachine::finished, machine, &QStateMachine::deleteLater); Для переключения состояний обычно используют сигналы от внешних по отношению к машине объектов. state1->addTransition(external_obj1, SIGNAL(customSignal()), state2); state2->addTransition(external_obj2, SIGNAL(customSignal()), final_state); Однако этот вариант не подходит, если таковых объектов не имеется. В этом случае можно добавить в класс, в котором существует объект машины, собственный произвольный сигнал. Например: class MyClass : public QObject { Q_OBJECT signals: void customSignal(); public: MyObject(QObject *parent = Q_NULLPTR) : QObject(parent) {} public slots: void run(); }; void MyClass::run() { QStateMachine *machine = new QStateMachine(this); connect(machine, &QStateMachine::stopped , machine, &QStateMachine::deleteLater); connect(machine, &QStateMachine::finished , machine, &QStateMachine::deleteLater); QState *state1 = new QState(machine); connect(state1, &QState::entered, [this]() { // Код при входе в первое состояние ... emit customSignal(); }); QState *state2 = new QState(machine); connect(state2, &QState::entered, [this]() { // Код при входе во второе состояние ... emit customSignal(); }); QFinalState *final_state = new QFinalState(machine); connect(final_state, &QFinalState::entered, [this]() { // Здесь, например, можно уведомить // пользователя о завершении работы. }); // Подключаем переходы. state1->addTransition(this, SIGNAL(customSignal()), state2); state2->addTransition(this, SIGNAL(customSignal()), final_state); // Указываем точку входа и активируем машину. machine->setInitialState(state1); machine->start(); } Однако такой подход приводит к проблемам при необходимости расширения типов реакции машины на каждое из состояний. Предположим, что теперь требуется учитывать успешно или неуспешно завершились state1 и state2. Конечно, можно добавить ещё несколько сигналов в MyClass, но что за "ёрш" в итоге получится, если и количество состояний будет расти? Очевидно, что в данной ситуации требуется знать не только результат выполнения кода при выходе из каждого состояния, но и, собственно, что это было за состояние из списка имеющихся. Помочь может использование событий и переходов (transition) по событиям. Для начала класс произвольного события stateevent.h: #include class QState; class StateEvent : public QEvent { public: enum Result { RESULT_NULL, RESULT_SUCCEED, RESULT_FAILED }; static const QEvent::Type &_event_type; StateEvent(QState *state, Result result); QState *state() const {return _state;} StateEvent::Result result() const {return _result;} private: QState *_state; Result _result; }; ... и stateevent.cpp: #include "stateevent.h" const QEvent::Type &StateEvent::_event_type = static_cast(QEvent::registerEventType()); StateEvent::StateEvent(QState *state, Result result) : QEvent(_event_type), _state(state), _result(result) {} А теперь и класс перехода: #include #include "stateevent.h" class StateTransition : public QAbstractTransition { Q_OBJECT public: StateTransition(QState *source_state = Q_NULLPTR) : QAbstractTransition(source_state) , _result(StateEvent::RESULT_NULL) {} StateEvent::Result eventResult() const {return _result;} void setEventResult(StateEvent::Result result) {_result = result;} protected: virtual bool eventTest(QEvent *event) { if(event->type() == StateEvent::_event_type) { StateEvent *state_event = static_cast(event); return (state_event->state() == sourceState() && state_event->result() == _result); } return false; } virtual void onTransition(QEvent *event) {Q_UNUSED(event);} private: StateEvent::Result _result; }; Метод eventTest(QEvent*) проверяет, соответствует ли объект состояния (QState) и результат выполнения (StateEvent::Result) объекту перехода (StateTransition). Если истина, то машина переключится в новое состояние, которое назначено при выполнении перечисленных условий. Теперь код активации машины меняется на следующий (произвольные сигналы нам больше не нужны): class MyClass : public QObject { Q_OBJECT public: MyObject(QObject *parent = Q_NULLPTR) : QObject(parent) {} public slots: void run(); }; void MyClass::run() { QStateMachine *machine = new QStateMachine(this); connect(machine, &QStateMachine::stopped , machine, &QStateMachine::deleteLater); connect(machine, &QStateMachine::finished , machine, &QStateMachine::deleteLater); QState *state1 = new QState(machine); connect(state1, &QState::entered, [this,state1]() { // Код при входе в первое состояние ... bool result = ... ; if(result == true) { state1->machine() ->postEvent(new StateEvent(state1 , StateEvent::RESULT_SUCCEED)); } else { state1->machine() ->postEvent(new StateEvent(state1 , StateEvent::RESULT_FAILED)); } }); QState *state2 = new QState(machine); connect(state2, &QState::entered, [this,state2]() { // Код при входе во второе состояние ... bool result = ... ; if(result == true) { state2->machine() ->postEvent(new StateEvent(state2 , StateEvent::RESULT_SUCCEED)); } else { state2->machine() ->postEvent(new StateEvent(state2 , StateEvent::RESULT_FAILED)); } }); QFinalState *final_state = new QFinalState(machine); connect(final_state, &QFinalState::entered, [this]() { // Здесь, например, можно уведомить // пользователя о завершении работы. }); // Подключаем переходы. { StateTransition *success_transition = new StateTransition(); success_transition->setEventResult(StateEvent::RESULT_SUCCEED); success_transition->setTargetState(state2); StateTransition *fail_transition = new StateTransition(); fail_transition->setEventResult(StateEvent::RESULT_FAILED); fail_transition->setTargetState(final_state); state1->addTransition(success_transition); state1->addTransition(fail_transition); } { StateTransition *success_transition = new StateTransition(); success_transition->setEventResult(StateEvent::RESULT_SUCCEED); success_transition->setTargetState(final_state); StateTransition *fail_transition = new StateTransition(); fail_transition->setEventResult(StateEvent::RESULT_FAILED); fail_transition->setTargetState(final_state); state2->addTransition(success_transition); state2->addTransition(fail_transition); } // Указываем точку входа и активируем машину. machine->setInitialState(state1); machine->start(); } С помощью продемонстрированного механизма можно свободно "плодить" различные типы результата работы для состояний, что в свою очередь позволит конечному автомату переключать эти самые состояния в той последовательности и по тем правилам, что заранее были для него определены. При отправке события о завершении работы каждого из состояний, например: state1->machine() ->postEvent(new StateEvent(state1 , StateEvent::RESULT_SUCCEED)); ... в StateEvent вносится и сохраняется указатель на объект состояния, из которого должен быть осуществлён переход. При этом метод QStateMachine::postEvent(QEvent*) отправит вновь созданное событие всем объектам перехода, которые были зарегистрированы на этапе создания конечного автомата, а те уже проверят самостоятельно, соответствует ли кому-нибудь из них это событие. При последовательном подключении состояний сохранять объект состояния (QState), из которого должен быть осуществлён переход, необязательно, поскольку в этом режиме работы в конечном автомате (QStateMachine) текущим может быть только одно состояние. Достаточно отправлять лишь сведения о результате выполнения. Но всё меняется, если состояния подключаются параллельно: одно событие переключит сразу нескольких, что во многих случаях приведёт к непредсказуемому результату. Сохранение же указателя на источник перехода позволит избежать обозначенной проблемы. Осталось поменять код из примера выше для параллельного подключения состояний. void MyClass::run() { QStateMachine *machine = new QStateMachine(QState::ParallelStates, this); connect(machine, &QStateMachine::stopped , machine, &QStateMachine::deleteLater); connect(machine, &QStateMachine::finished , machine, &QStateMachine::deleteLater); for(int i = 0; i < 2; ++i) { QState *parent_state = new QState(machine); QState *state = new QState(parent_state); connect(state, &QState::entered, [this,state]() { // Код при входе в **i-ое** состояние ... bool result = ... ; if(result == true) { state->machine() ->postEvent(new StateEvent(state , StateEvent::RESULT_SUCCEED)); } else { state->machine() ->postEvent(new StateEvent(state , StateEvent::RESULT_FAILED)); } }); QFinalState *final_state = new QFinalState(parent_state); StateTransition *success_transition = new StateTransition(); success_transition->setEventResult(StateEvent::RESULT_SUCCEED); success_transition->setTargetState(final_state); StateTransition *fail_transition = new StateTransition(); fail_transition->setEventResult(StateEvent::RESULT_FAILED); fail_transition->setTargetState(final_state); state->addTransition(success_transition); state->addTransition(fail_transition); } // Активируем машину (указывать точку входа нужды нет). machine->start(); } Поскольку QStateMachine является наследником QState мы имеем возможность объявить посредством установки флага QState::ParallelStates, что дочерние состояния машины должны быть подключены в параллельном режиме. Это указание действует только на тех из них, для которых объект конечного автомата является непосредственным родителем. Таким образом в теле цикла в параллельный режим войдут только копии parent_state, тогда как уже их дочерние состояния останутся по умолчанию - подключенными последовательно. Разумеется, что именно такое построение как в примере вовсе необязательно и объекты QState (в т.ч. QStateMachine) можно компоновать с любым количеством иерархических уровней, однако последний, самый низкий уровень должен быть представлен в виде последовательного подключения как минимум одного QState и QFinalState, поскольку только тогда "наверх" по иерархии будет отправлен сигнал о завершении работы. Да, можно обойтись даже и одним QFinalState, но тогда необходимо иметь в виду, что сигнал finished() будет отправлен прежде, чем будет выполнен вход в состояние QFinalState, а значит код, ассоциированный с ним, будет также выполнен позднее. Для многих задач это бывает неприемлемо. Каждый из parent_state в примере будет выполняться параллельно по отношению к другому, и может закончить свою работу раньше, либо позднее других. Однако при этом QStateMachine не остановится, несмотря на то, что от одного из выполнившихся parent_state к нему придёт сигнал finished(). Лишь только тогда, когда все parent_state завершат свою работу, только тогда конечный автомат завершит и свою, отправив finished(), но уже от себя. QStateMachine чрезвычайно мощный и гибкий инструмент, и совершенно напрасно многие считают его избыточным, либо подходящим лишь для подключения анимации. Этот фреймворк содержится в модуле QtCore и может быть весьма полезен для реализации практически любых задач, требующих, прежде всего, асинхронного выполнения.

Комментариев нет:

Отправить комментарий