Deep understanding of state machine

Posted by jamiethehill on Sat, 19 Feb 2022 01:22:33 +0100

catalogue

1. What is a state machine

2. State machine in life

3. Application in Engineering

4. Divergent thinking

1. What is a state machine

■ 1.1 directed graph

  • A directed graph is a set of fixed points and edges. We often use G (E, V) to represent a directed graph, in which the fixed points are connected by directed edges to form a directed graph. Directed graph has many practical meanings in real life, such as topological sorting (in short, constantly removing nodes without penetration) and digestra algorithm and graph database are the applications of directed graph in network addressing. From another point of view, a directed graph seems to be an abstraction of an actual scene.

■ 1.2 state machine and directed graph

  • In fact, the transformation between state machines is closely related to the directed graph. For example, to understand the state machine, a person walks on a map with several cities. He starts from a starting point and chooses different roads according to his preferences, so as to go to different cities. This example contains two important information. One is the city and the other is the road. Then they correspond to the fixed points and edges in the directed graph, and the state and transition in the state machine.

  • Therefore, if you want to make good use of the state machine mode in the actual development, the first step is to design your own state machine transformation diagram. Only by reasonably designing the logic of state transformation can you make the logic of the code more cheap, simple and clear. In other words, when you find a destination on a simple map, it must be much easier than on a complex map.

■ 1.3 what does Android in Google do? (mediaplayer)

■ 1.4 several important concepts of state machine

  • State state:
    A state machine must contain at least two states. For example, the above example of automatic door has two states: open and closed.

  • Event:
    An event is a trigger condition or password to perform an operation. For automatic doors, "pressing the door open button" is an event.

  • Action:
    Actions to be performed after the event. For example, the event is "press the door open button" and the Action is "open the door". When programming, an Action usually corresponds to a function.

  • Transition Transformation:
    That is, change from one state to another. For example, "door opening process" is a transformation

    Of course, the concept is always specified. In the actual project, we may need to adjust our specific implementation according to the actual needs.


In this example, Idle is only allowed when the program is running

The change of state and the occurrence of events often trigger the change of state. For this example,
setDataSourece is an event. In this event, Mediaplayer
The status is switched, idle - > initialized

Often, the setting of a state machine will contain a start state and an end state

■ 1.5 scenarios and benefits

  The most common application scenario is the control of personas in the game, such as controlling a soldier (shooting, running and jumping, etc.) when there are a large number of people

Hierarchical state machines can also be used for control.

 So what are the benefits of using a state machine?
1. When a program has multiple states, it standardizes the state transition of the state machine and avoids introducing some complex judgment logic.
2. It standardizes the capabilities that the program can provide in different states.
3. The capability can be expanded horizontally to provide a new state to improve the existing logic

■ 1.6 summary and review

  • When introducing the state machine model into practical engineering, the most important thing is to determine the directed graph of the whole state machine, and determine what states, events and actions there are.

2. State machine in life

■ 2.1 introduction to this section

  • In fact, there are many examples of state machines in life. I think the simplest may be a light bulb. Imagine a simple light bulb. It may have several states: off - > low - > medium - > High - > off. The content of this section is to introduce the programming idea of state machine to you through an example.
    The following will optimize the example of light bulb step by step to organize it into the code of state machine design pattern.

■ 2.2 first coding

  • In this encoding, we define an enumeration type that contains all bulb States, and also define a state transition table. The state transition table specifically defines the next state that each state can transition, avoiding the conversion of wrong states. This may also be the simplest state machine. Define a state in the class and switch according to different events.

  • But what if we take the state out of the enumerated variables? This is the first step we need to do. Write each state as a separate class. But at the same time, we should pay attention to a problem. The state class must be single instance, because we may have to switch back to the same state of the last time every time we switch. We don't need to apply for memory release again.

■ 2.3 second coding



■ 2.4 summary and review

  • The state machine is nothing more than the definition and switching of states. For some simple examples, switch may be used to complete the relevant coding. For some complex examples, we may need to abstract specific states and business entities to facilitate business expansion, but this is not necessarily absolute.
  • After reading this example, I think you should have a preliminary understanding of the state machine, at least not entangled in the boring concepts in the first section. So how can we combine the design model of state machine in practical engineering? I spent a lot of time thinking about this problem, consulted the relevant code, and looked through the implementation of mediaplayer in Android. To my surprise, the Android source code is not so abstract as to encapsulate the state machine, and does not abstract each state into a separate class. One of the biggest benefits of this approach is to avoid the definition of a large number of state classes, But it doesn't mean that we can't do this. In the next section, I will try to use a general tool class to complete the development of a "mediaplayer", and show you the actual effect for further discussion.

3. Application in Engineering

  • In the first section, we mentioned Android's mediaplayer, so let's use the state machine to simulate a complex scenario. Look first
    Demo demo
  • Source code
  • Class diagram with Supplement
#include "tinyfsm.hpp"
#include <iostream>

class Idle;
class End;
class Error;
class Initialized;
class Prepared;
class Started;
class Stoped;
class Completed;
class Paused;
class Preparing;

#define EVENT_CODE_BASE     0
#define EVENT_SET_DATA_SOURCE       EVENT_CODE_BASE + 1
#define EVENT_SET_PREPARE_ASYNC     EVENT_CODE_BASE + 2
#define EVENT_SET_RESET             EVENT_CODE_BASE + 3


#define EVENT_INVAILED_INPUT        EVENT_CODE_BASE - 1


struct MedieEvent : tinyfsm::Event {
    int code;
};

class MediaPlayer {
public:

private:
    friend class Idle;
    friend class Idle;
    friend class End;
    friend class Error;
    friend class Initialized;
    friend class Prepared;
    friend class Started;
    friend class Stoped;
    friend class Completed;
    friend class Paused;

    void setDataSource(MedieEvent* event) {
        std::cout << "MediaPlayer setDataSource working" << std::endl;
    };

    void prepareAync(MedieEvent* event) {
        std::cout << "MediaPlayer prepareAync working" << std::endl;
    };

    void resetReasource(MedieEvent* event) {
        std::cout << "MediaPlayer resetReasource working" << std::endl;
    };

    int sourceCount;
};


struct SetDataSource : MedieEvent
{
    SetDataSource(MediaPlayer* pMedia):m_pMediaPlayer(pMedia){
        code = EVENT_SET_DATA_SOURCE;
    }

    MediaPlayer* m_pMediaPlayer;
};

struct PreparedAsync : MedieEvent {

    PreparedAsync(MediaPlayer* pMedia) :m_pMediaPlayer(pMedia) {
        code = EVENT_SET_PREPARE_ASYNC;
    }
    MediaPlayer* m_pMediaPlayer;
};

struct InviledInput : MedieEvent
{
    InviledInput(MediaPlayer* pMedia) :m_pMediaPlayer(pMedia) {
        code = EVENT_INVAILED_INPUT;
    }
    MediaPlayer* m_pMediaPlayer;
};

struct Reset :MedieEvent {

    Reset(MediaPlayer* pMedia) :m_pMediaPlayer(pMedia) {
        code = EVENT_SET_RESET;
    }
    MediaPlayer* m_pMediaPlayer;
};


struct MediaState : tinyfsm::Fsm<MediaState> {
    virtual void react(MedieEvent* ) { };

    // alternative: enforce handling of Toggle in all states (pure virtual)
    //virtual void react(Toggle const &) = 0;

    virtual void entry(MediaPlayer* pM) { };  /* entry actions in some states */
    virtual void exit(MediaPlayer* pM) { };  /* no exit actions */
};

//_ state_instance because of the template class and static, these classes are singletons
class Error : public MediaState {
public:
    void react(MedieEvent* event) override{
        if (nullptr == event)
            return;

        switch (event->code)
        {
        case EVENT_INVAILED_INPUT: {
        
            std::cout << "Error: your input wrong! what should I do !" << std::endl;

            break;
        }
        case EVENT_SET_RESET: {
            std::cout << "Error: EVENT_SET_RESET working" << std::endl;
            Reset* resetEvent = static_cast<Reset*>(event);
            if (nullptr == resetEvent)
                return;

            resetEvent->m_pMediaPlayer->resetReasource(resetEvent);


            transit<Idle>(resetEvent->m_pMediaPlayer);

            break;
        }
        default:
            std::cout << "Error: current status can't execute your command" << std::endl;
            break;
        }
    }

    void entry(MediaPlayer* pM) override {
        std::cout << "Error: entry" << std::endl;
        //do something about me
        pM->sourceCount = 0;

    }

    void exit(MediaPlayer* pM) override {
        std::cout << "Error: exit" << std::endl;
    }
};

class Idle : public MediaState {
public:
    void react(MedieEvent* event) override{
        std::cout << "Idle: react" << std::endl;
        if (nullptr == event)
            return;

        if (event->code < 0) {
            // TODO (songxufei): defines the base class of error
            InviledInput* invaledEvent = static_cast<InviledInput*>(event);

            //transmit to error state
            transit<Error>(invaledEvent->m_pMediaPlayer);

            //do our repair work
            dispatch<MedieEvent>(event);
            return;
        }


        switch (event->code)
        {
        case EVENT_SET_DATA_SOURCE: {
            std::cout << "Idle: setDataSource working" << std::endl;
            SetDataSource* eventSetSource = static_cast<SetDataSource*>(event);
            if (nullptr == eventSetSource)
                return;

            eventSetSource->m_pMediaPlayer->setDataSource(eventSetSource);


            transit<Initialized>(eventSetSource->m_pMediaPlayer);

            break;
        }
        default:
            std::cout << "Error: current status can't execute your command" << std::endl;
            break;
        }

        MediaState::react(event);
    }

    void entry(MediaPlayer* pM) override{
        std::cout << "Idle: entry" << std::endl;
        //do something about me
        pM->sourceCount = 0;

    }

    void exit(MediaPlayer* pM) override{ 
        std::cout << "Idle: exit" << std::endl;
    }
};


class Initialized : public MediaState {
public:
    void react(MedieEvent* event) override {
        std::cout << "Initialized: react" << std::endl;
        if (nullptr == event)
            return;

        if (event->code < 0) {
            // TODO (songxufei): defines the base class of error
            InviledInput* invaledEvent = static_cast<InviledInput*>(event);

            //transmit to error state
            transit<Error>(invaledEvent->m_pMediaPlayer);

            //do our repair work
            dispatch<MedieEvent>(event);
            return;
        }


        switch (event->code)
        {
        case EVENT_SET_PREPARE_ASYNC: {
            std::cout << "Initialized: prepare async working" << std::endl;
            PreparedAsync* preEvent = static_cast<PreparedAsync*>(event);
            if (nullptr == preEvent ) {
                std::cout << "Initialized: prepare async error" << std::endl;
                return;
            }

            preEvent->m_pMediaPlayer->prepareAync(preEvent);

            //The pointer brought in by transmist is easy to operate the corresponding resources
            transit<Preparing>(preEvent->m_pMediaPlayer);
            break;
        }
        default:
            std::cout << "Initialized: current status can't execute your command" << std::endl;
            break;
        }
    }

    void entry(MediaPlayer* pM) override {
        std::cout << "Initialized: entry" << std::endl;
      
    }

    void exit(MediaPlayer* pM) override {
        std::cout << "Initialized: exit" << std::endl;
    }
};

class Preparing : public MediaState {
public:
    void react(MedieEvent* event) override {
        std::cout << "Preparing: react" << std::endl;
        if (nullptr == event)
            return;

        if (event->code < 0) {
            // TODO (songxufei): defines the base class of error
            InviledInput* invaledEvent = static_cast<InviledInput*>(event);

            //transmit to error state
            transit<Error>(invaledEvent->m_pMediaPlayer);

            //do our repair work
            dispatch<MedieEvent>(event);
            return;
        }

        switch (event->code)
        {
        default:
            std::cout << "Preparing: current status can't execute your command" << std::endl;
            break;
        }
    }

    void entry(MediaPlayer* pM) override {
        std::cout << "Preparing: entry" << std::endl;

    }

    void exit(MediaPlayer* pM) override {
        std::cout << "Preparing: exit" << std::endl;
    }
};

//init state with idle
FSM_INITIAL_STATE(MediaState, Idle)

using fsm_handle = MediaState;

//Only use template classes with different addresses
template<typename X>
class C {
public:
    static int a;
};
template<typename X>
int C<X>::a = 0;

class D : public C<D> {

};

int main()
{
    printf("##1 %ld  ", (long)&(C<int>::a));
    printf("##2 %ld  ", (long)&(D::a));

    MediaPlayer* player = new MediaPlayer();
    fsm_handle::start(player);

    std::cout << "EVENT_SET_DATA_SOURCE : 1 " << std::endl ;
    std::cout << "EVENT_SET_PREPARE_ASYNC : 2 " << std::endl;
    std::cout << "EVENT_SET_RESET : 3 " << std::endl;

    while (1)
    {
        int n;
        std::cout << std::endl << "1 - 100=Event id, 0=Quit ? ";
        std::cin >> n;
        switch (n) {
        case EVENT_SET_DATA_SOURCE: {
            SetDataSource* event = new SetDataSource(player);
            fsm_handle::dispatch<MedieEvent>(event);
            std::cout << "deleteing event resource " << std::endl;
            delete event;
            break;
        }
        case EVENT_SET_PREPARE_ASYNC: {
            PreparedAsync* event = new PreparedAsync(player);
            fsm_handle::dispatch<MedieEvent>(event);
            std::cout << "deleteing event resource " << std::endl;
            delete event;
            break;
        }
        case EVENT_SET_RESET: {
            Reset* event = new Reset(player);
            fsm_handle::dispatch<MedieEvent>(event);
            std::cout << "deleteing event resource " << std::endl;
            delete event;
            break;
        }
                           
        case 0:
            goto del;
        default: {
            std::cout << "> Invalid input" << std::endl;
            InviledInput* event = new InviledInput(player);
            fsm_handle::dispatch<MedieEvent>(event);
            delete event;
            break;
        }
        }
    }
del:
    delete player;
    return 0;
}
  • Before looking at encapsulation, you should review some knowledge of C + +
  1. Nested dependent types of templates
    Class used to declare in template class
    for example
    template
    void fun(const T& proto ,typename T::const_iterator it);

  2. std::is_same
    It can judge whether the two types are the same, especially in the template. When the parameters of the template are not clear, this function can specify some specific parameter types
    Special treatment.

  3. Constexpr keyword
    Tells the compiler to perform compile time calculations

  • Source code
namespace tinyfsm
{

    // --------------------------------------------------------------------------

    struct Event { };

    // --------------------------------------------------------------------------

#ifdef TINYFSM_NOSTDLIB
  // remove dependency on standard library (silent fail!).
  // useful in conjunction with -nostdlib option, e.g. if your compiler
  // does not provide a standard library.
  // NOTE: this silently disables all static_assert() calls below!
    template<typename F, typename S>
    struct is_same_fsm { static constexpr bool value = true; };
#else
    //Here is an inheritance
  // check if both fsm and state class share same fsmtype
    template<typename F, typename S>
    struct is_same_fsm : std::is_same< typename F::fsmtype, typename S::fsmtype > { };
    
#endif

    template<typename S>
    struct _state_instance
    {
        using value_type = S;
        using type = _state_instance<S>;
        //This is equivalent to each class being a simple interest class
        static S value;
    };
    //Here is the definition of a static variable
    template<typename S>
    typename _state_instance<S>::value_type _state_instance<S>::value;

    // --------------------------------------------------------------------------

    template<typename F>
    class Fsm
    {
    public:

        using fsmtype = Fsm<F>;
        using state_ptr_t = F*;

        static state_ptr_t current_state_ptr;

        // public, leaving ability to access state instance (e.g. on reset)
        template<typename S>
        static constexpr S& state(void) {
            static_assert(is_same_fsm<F, S>::value, "accessing state of different state machine");
            return _state_instance<S>::value;
        }

        template<typename S>
        static constexpr bool is_in_state(void) {
            static_assert(is_same_fsm<F, S>::value, "accessing state of different state machine");
            return current_state_ptr == &_state_instance<S>::value;
        }

        /// state machine functions
    public:

        // explicitely specialized in FSM_INITIAL_STATE macro
        static void set_initial_state();

        static void reset() { };

        template<typename P>
        static void enter(P* p) {
            current_state_ptr->entry(p);
        }

        static void enter() {
            current_state_ptr->entry();
        }

        static void start() {
            set_initial_state();
            enter();
        }

        // This S is user base class point
        template<typename P>
        static void start(P* p) {
            set_initial_state();
            enter(p);
        }

        template<typename E>
        static void dispatch(E const& event) {
            current_state_ptr->react(event);
        }

        template<typename E>
        static void dispatch(E* const event) {
            current_state_ptr->react(event);
        }

        /// state transition functions
    protected:
        static std::mutex mutexTransmit;

        template<typename S>
        void transit(void) {
            std::unique_lock<std::mutex> lk(mutexTransmit);

            static_assert(is_same_fsm<F, S>::value, "transit to different state machine");
            current_state_ptr->exit();
            current_state_ptr = &_state_instance<S>::value;
            current_state_ptr->entry();
        }
        // add this for our stitucation, add by sxf
        template<typename S, typename M>
        void transit(M* pM) {
            std::unique_lock<std::mutex> lk(mutexTransmit);
            static_assert(is_same_fsm<F, S>::value, "transit to different state machine");
            current_state_ptr->exit(pM);
            current_state_ptr = &_state_instance<S>::value;
            current_state_ptr->entry(pM);
        }

        template<typename S, typename ActionFunction>
        void transit(ActionFunction action_function) {
            std::unique_lock<std::mutex> lk(mutexTransmit);

            static_assert(is_same_fsm<F, S>::value, "transit to different state machine");
            current_state_ptr->exit();
            // NOTE: we get into deep trouble if the action_function sends a new event.
            // TODO: implement a mechanism to check for reentrancy
            action_function();
            current_state_ptr = &_state_instance<S>::value;
            current_state_ptr->entry();
        }

        template<typename S, typename ActionFunction, typename ConditionFunction>
        void transit(ActionFunction action_function, ConditionFunction condition_function) {
            if (condition_function()) {
                transit<S>(action_function);
            }
        }
    };

    //Initializes a static variable of F *
    template<typename F>
    typename Fsm<F>::state_ptr_t Fsm<F>::current_state_ptr;

    template<typename F>
    std::mutex  Fsm<F> ::mutexTransmit;

    // --------------------------------------------------------------------------

    template<typename... FF>
    struct FsmList;

    template<> struct FsmList<> {
        static void set_initial_state() { }
        static void reset() { }
        static void enter() { }
        template<typename E>
        static void dispatch(E const&) { }
    };

    template<typename F, typename... FF>
    struct FsmList<F, FF...>
    {
        using fsmtype = Fsm<F>;

        static void set_initial_state() {
            fsmtype::set_initial_state();
            FsmList<FF...>::set_initial_state();
        }

        static void reset() {
            F::reset();
            FsmList<FF...>::reset();
        }

        static void enter() {
            fsmtype::enter();
            FsmList<FF...>::enter();
        }

        static void start() {
            set_initial_state();
            enter();
        }

        template<typename E>
        static void dispatch(E const& event) {
            fsmtype::template dispatch<E>(event);
            FsmList<FF...>::template dispatch<E>(event);
        }
    };

    // --------------------------------------------------------------------------

    template<typename... SS> struct StateList;
    template<> struct StateList<> {
        static void reset() { }
    };
    template<typename S, typename... SS>
    struct StateList<S, SS...>
    {
        static void reset() {
            //_state_instance<S>::value.exit();
            _state_instance<S>::value = S();
            StateList<SS...>::reset();
        }
    };

    // --------------------------------------------------------------------------

    template<typename F>
    struct MooreMachine : tinyfsm::Fsm<F>
    {
        virtual void entry(void) { };  /* entry actions in some states */
        void exit(void) { };           /* no exit actions */
    };

    template<typename F>
    struct MealyMachine : tinyfsm::Fsm<F>
    {
        // input actions are modeled in react():
        // - conditional dependent of event type or payload
        // - transit<>(ActionFunction)
        void entry(void) { };  /* no entry actions */
        void exit(void) { };   /* no exit actions */
    };

} /* namespace tinyfsm */


#define FSM_INITIAL_STATE(_FSM, _STATE)                               \
namespace tinyfsm {                                                   \
  template<> void Fsm< _FSM >::set_initial_state(void) {              \
    current_state_ptr = &_state_instance< _STATE >::value;            \
  }                                                                   \
}

4. Divergent thinking

■ 4.1 message oriented programming
At the beginning of this article, I introduced some related contents of directed graph. Directed graph is a very interesting data structure. It seems to simulate many scenes in our life. When we regard each fixed point as a state, it is our state graph. So what I want to introduce next is to treat each fixed point as a collaborative process. What will it be like for a specific person to apply them in programming? The business is driven by messages without lock restrictions. This is the actor model proposed by Carl Hewitt in 1973. It is also a programming model I have always liked. Actor can be imagined as an object instance in object-oriented programming language. The difference is that the state of actor cannot be read and modified directly, and the method cannot be called directly. Actor can only communicate with the outside world through message passing. Each participant has an address representing itself, but can only send messages to that address. As shown below.