c++ – Concurrent states in QStateMachine

Question:

The Qt help describes in some detail the use of the QStateMachine state machine in the case of sequentially changing states. However, the implementation of an automaton with parallelization has no examples and is described only by a diagram, and a couple of paragraphs of text in English that do not bring much clarity.

How to implement parallel states and transitions between them in QStateMachine ? Separately, I would like to see the organization of transitions by using events instead of signals, since the latter are not convenient to use in all cases.

Answer:

The implementation of parallel states in the Qt State Machine Framework, unlike sequential states, has one significant feature. But for the sake of completeness, let's start with sequential ones. Let's create a car and a couple of states.

QStateMachine *machine = new QStateMachine();
QState *state1 = new QState(machine);
QState *state2 = new QState(machine);

If it is assumed for our machine that it, under certain conditions (say, when passing two previously declared states), must complete its work, then it is convenient to add a completion state to it.

QFinalState *final_state = new QFinalState(machine);

Using QFinalState convenient because when an execution context enters it, this type of state automatically sends a finished() signal to its parent. If the parent is a QStateMachine object, then the machine will be stopped; if it is a QState object, then that one in turn will send the same signal up the chain. This feature is also fully used when states are connected in parallel, which will be discussed below.

By the way, in all examples of Qt-help, the machine has an indefinite lifetime, while in practice there are often tasks that, on the one hand, require asynchronous execution (for example, sending and processing network requests), and on the other hand, they arise as needed . There is no interest in keeping in memory a machine object with all states, if, say, something needs to be done regularly, but not constantly.

For such cases, when declaring the machine, you can simply connect the appropriate signals and the machine object will destroy itself and all child objects as soon as it completes.

connect(machine, &QStateMachine::stopped, machine, &QStateMachine::deleteLater);
connect(machine, &QStateMachine::finished, machine, &QStateMachine::deleteLater);

To switch states, signals from objects external to the machine are usually used.

state1->addTransition(external_obj1, SIGNAL(customSignal()), state2);
state2->addTransition(external_obj2, SIGNAL(customSignal()), final_state);

However, this option is not suitable if there are no such objects. In this case, you can add your own arbitrary signal to the class in which the machine object exists. For instance:

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();
}

However, this approach leads to problems if it is necessary to expand the types of machine reactions to each of the states.

Let's assume that we now need to take into account the success or failure state1 and state2 . Of course, you can add a few more signals to MyClass , but what kind of "ruff" will you end up with if the number of states also grows? Obviously, in this situation, it is required to know not only the result of the code execution when exiting each state, but also, in fact, what kind of state it was from the list of available ones. Use of events and transitions ( transition ) on events can help.

First, the custom event class stateevent.h :

#include <QtCore/QEvent>

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::Type>(QEvent::registerEventType());

StateEvent::StateEvent(QState *state, Result result)
    : QEvent(_event_type), _state(state), _result(result) {}

And now the transition class:

#include <QtCore/QAbstractTransition>

#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<StateEvent*>(event);
                return (state_event->state() == sourceState()
                    && state_event->result() == _result);
            }

            return false;
        }

        virtual void onTransition(QEvent *event) {Q_UNUSED(event);}

    private:
        StateEvent::Result _result;
};

The eventTest(QEvent*) method checks whether the state object ( QState ) and execution result ( StateEvent::Result ) match the transition object ( StateTransition ). If true, the machine will switch to the new state assigned when the above conditions are met.

Now the activation code of the machine changes to the following (we no longer need arbitrary signals):

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();
}

Using the demonstrated mechanism, you can freely "produce" various types of work results for states, which in turn will allow the finite automaton to switch these states in the same sequence and according to the rules that were previously defined for it.

When sending an event about the completion of each of the states, for example:

state1->machine()
    ->postEvent(new StateEvent(state1
        , StateEvent::RESULT_SUCCEED));

… a pointer to the state object from which the transition is to be made is inserted and stored in the StateEvent . In this case, the QStateMachine::postEvent(QEvent*) method will send the newly created event to all transition objects that were registered at the stage of creating the state machine, and they will already check on their own whether this event matches any of them.

When daisy chaining states, it is not necessary to save the state object ( QState ) from which the transition should be made, since in this mode of operation in the state machine ( QStateMachine ) only one state can be current. It is enough to send only information about the result of the execution. But everything changes if the states are connected in parallel: one event will switch several at once, which in many cases will lead to an unpredictable result. Saving a pointer to the source of the transition will avoid the indicated problem.

It remains to change the code from the example above for connecting states in parallel.

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();
}

Because QStateMachine is a subclass of QState , we can declare by setting the QState::ParallelStates flag that the machine's child states should be connected in parallel. This hint only affects those for which the state machine object is the immediate parent.

Thus, in the loop body, only copies of parent_state will enter the parallel mode, while their child states will remain by default – connected in series.

Of course, such a construction as in the example is not at all necessary and QState objects (including QStateMachine ) can be combined with any number of hierarchical levels, however, the last, lowest level must be represented as a serial connection of at least one QState and QFinalState , because only then "up" in the hierarchy will be sent a signal to complete the work.

Yes, even one QFinalState can be enough, but then you need to keep in mind that the finished() signal will be sent before the QFinalState is entered, which means that the code associated with it will also be executed later. For many tasks, this is unacceptable.

Each of the parent_state in the example will be executed in parallel with respect to the other, and may finish its work earlier or later than the others. However, this does not stop the QStateMachine , despite the fact that one of the executed parent_state will send a finished() signal to it. Only when all parent_state completed their work, only then will the state machine complete its own, sending finished() , but from itself.

QStateMachine extremely powerful and flexible tool, and in vain many consider it redundant, or suitable only for connecting animation. This framework is contained in the QtCore module and can be very useful for implementing almost any task that requires, first of all, asynchronous execution.

Scroll to Top