Страницы

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

пятница, 26 октября 2018 г.

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

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


Ответ

Реализация параллельных состояний в 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 и может быть весьма полезен для реализации практически любых задач, требующих, прежде всего, асинхронного выполнения.

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

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