Boost MSM parallel behavior with delayed self-transitions?

ε祈祈猫儿з 提交于 2019-12-01 12:50:27
Takatoshi Kondo

Here is a complete code example to do that:

// g++ example.cpp -lboost_system

#include <iostream>

#include <boost/asio.hpp>

#include <boost/msm/back/state_machine.hpp>
#include <boost/msm/front/state_machine_def.hpp>
#include <boost/msm/front/functor_row.hpp>

namespace msm = boost::msm;
namespace msmf = boost::msm::front;
namespace mpl = boost::mpl;


// ----- State machine
struct Sm : msmf::state_machine_def<Sm> {
    using back = msm::back::state_machine<Sm>;

    template <typename... T>
    static std::shared_ptr<back> create(T&&... t) {
        auto p = std::make_shared<back>(std::forward<T>(t)...);
        p->wp = p; // set wp after creation.
        return p;
    }

    template <typename Ev>
    void process(Ev&& ev) {
        // process_event via backend weak_ptr
        wp.lock()->process_event(std::forward<Ev>(ev));
    }

    // ----- Events
    struct EvSetParent {};
    struct After2 {};
    struct After5 {};

    Sm(boost::asio::io_service* ios):ios(ios) {}
    struct State1_:msmf::state_machine_def<State1_> {
        template <class Event,class Fsm>
        void on_entry(Event const&, Fsm& f) const {
            BOOST_STATIC_ASSERT((boost::is_convertible<Fsm, Sm>::value));
            std::cout << "State1::on_entry()" << std::endl;
            f.process(EvSetParent());
        }

        struct Action {
            template <class Event, class Fsm, class SourceState, class TargetState>
            void operator()(Event const&, Fsm&, SourceState&, TargetState&) const {
                std::cout << "Trying again..." << std::endl;
            }
        };

        struct A:msmf::state<> {
            template <class Event,class Fsm>
            void on_entry(Event const&, Fsm& f) const {
                BOOST_STATIC_ASSERT((boost::is_convertible<Fsm, State1_>::value));
                std::cout << "A::on_entry()" << std::endl;
                auto t = std::make_shared<boost::asio::deadline_timer>(*f.parent->ios);
                t->expires_from_now(boost::posix_time::seconds(2));
                t->async_wait([t, &f](boost::system::error_code const) {
                        f.parent->process(After2());
                    }
                );
            }
        };

        struct B:msmf::state<> {
            template <class Event,class Fsm>
            void on_entry(Event const&, Fsm& f) const {
                BOOST_STATIC_ASSERT((boost::is_convertible<Fsm, State1_>::value));
                std::cout << "B::on_entry()" << std::endl;
                auto t = std::make_shared<boost::asio::deadline_timer>(*f.parent->ios);
                t->expires_from_now(boost::posix_time::seconds(5));
                t->async_wait([t, &f](boost::system::error_code const) {
                        f.parent->process(After5());
                    }
                );
            }
        };

        // Set initial state
        typedef mpl::vector<A, B> initial_state;
        // Transition table
        struct transition_table:mpl::vector<
            //          Start  Event   Next       Action      Guard
            msmf::Row < A,     After2, A,         Action,     msmf::none >,
            msmf::Row < B,     After5, B,         Action,     msmf::none >
        > {};

        Sm* parent;
    };

    typedef msm::back::state_machine<State1_> State1;

    // Set initial state
    typedef State1 initial_state;

    struct ActSetParent {
        template <class Event, class Fsm, class SourceState, class TargetState>
        void operator()(Event const&, Fsm& f, SourceState& s, TargetState&) const {
                std::cout << "ActSetIos" << std::endl;
                s.parent = &f; // set parent state machine to use process() in A and B.
        }
    };
    // Transition table
    struct transition_table:mpl::vector<
        //          Start   Event        Next        Action        Guard
        msmf::Row < State1, EvSetParent, msmf::none, ActSetParent, msmf::none >
    > {};

    // front-end can access to back-end via wp.
    std::weak_ptr<back> wp;

    boost::asio::io_service* ios; // use pointer intentionally to meet copy constructible
};


int main() {
    boost::asio::io_service ios;
    auto t = std::make_shared<boost::asio::deadline_timer>(ios);

    auto sm = Sm::create(&ios);

    ios.post(
        [&]{
            sm->start();
        }
    );

    ios.run();
}

Let's digging the code.

Boost.MSM doesn't support delayed event fire mechanism. So we need to some timer handling mechanism. I choose Boost.Asio deadline timer. It works well with event driven library such as Boost.MSM.

In order to call process_event() in the front-end of the state machine, it needs to know its back-end. So I wrote create() function.

    template <typename... T>
    static std::shared_ptr<back> create(T&&... t) {
        auto p = std::make_shared<back>(std::forward<T>(t)...);
        p->wp = p; // set wp after creation.
        return p;
    }

It creates a shared_ptr of the back-end and then, and assigns it to the weak_ptr. If the weak_ptr set correctly, then I can call process_event() as follows. I wrote a wrapper process().

    template <typename Ev>
    void process(Ev&& ev) {
        // process_event via backend weak_ptr
        wp.lock()->process_event(std::forward<Ev>(ev));
    }

Client code call the create() function as follows:

    auto sm = Sm::create(&ios);

Sm has the member variable ios to set deadline timer. The front-end of the state-machine is required copyable by MSM. So ios is the pointer of io_service not reference.

State A and B are orthogonal regions. In order to implement orthogonal regions, define multiple initial states as mpl::vector.

    typedef mpl::vector<A, B> initial_state;

State A and B is composite states. MSM uses sub-machine state to implement composite states. Outer most state Sm is a state-machine and State1_ is also state machine. I set a timer in the entry action of state A and B. And when timer is fired, call process(). However, processs() is a member function of Sm, not State1_. So I need to implement some mechanism to access Sm from Stete1_. I added the member variable parent to State1_. It's a pointer of Sm. In the entry action of the State1_, I call process() and the event is PEvSetParent. It simply invokesActSetParent. In the action, SourceState isState1_`. I set parent member variable to parent pointer as follows:

    struct ActSetParent {
        template <class Event, class Fsm, class SourceState, class TargetState>
        void operator()(Event const&, Fsm& f, SourceState& s, TargetState&) const {
                std::cout << "ActSetIos" << std::endl;
                s.parent = &f; // set parent state machine to use process() in A and B.
        }
    };

Finally, I can call process() in the action of the state A and B.

        struct A:msmf::state<> {
            template <class Event,class Fsm>
            void on_entry(Event const&, Fsm& f) const {
                BOOST_STATIC_ASSERT((boost::is_convertible<Fsm, State1_>::value));
                std::cout << "A::on_entry()" << std::endl;
                auto t = std::make_shared<boost::asio::deadline_timer>(*f.parent->ios);
                t->expires_from_now(boost::posix_time::seconds(2));
                t->async_wait([t, &f](boost::system::error_code const) {
                        f.parent->process(After2());
                    }
                );
            }
        };

Edit

  1. How does this compare with a pthreads implementation? Do you think Boost.Asio is a better solution than putting the states A and B into different threads and having blocking, passive waits in each (such as what could be achieved via usleep(useconds_t usec) of unistd.h)? My feeling is that pthreads, which I have not tried using with Boost.MSM, would be a more generic/less constrained implementation?

Boost.MSM's process_event() is NOT thread-safe. So you need to lock it. See Thread safety in Boost msm AFAIK, sleep()/usleep()/nanosleep() are blocking functions. When you call them in the action of Boost.MSM, that means they are called (ogirinally) from process_event(). And it requires lock. Finally, blocking wait blocks each other (in this case, after2 and after5). Hence I think that Boost.ASIO's async approch is better.

  1. I am not clear on how the create and process methods work (why does the create function need a variadic template?). In particular, I've not previously worked with smart pointers or std::forward, so if you could give a human explanation of each line in these functions it would be great (I'm short on time to read about these features generically in order to try to understand this code).

Boost.MSM's backend inherits its frontend. The frontend constructor is Sm(boost::asio::io_service* ios):ios(ios) {}. In this case, the parameter of the constructor is ios. However, it could be changed depends on usecase. The function create() creates a shared_ptr of back. And back's constructor forwards all parameters to frontend. So the argument ios at auto sm = Sm::create(&ios); is forwarded to Sm's constructor. The reason I use variadic templates and std::forward is maximize flexibility. If the parameters of Sm's constructor is changed, I don't need to change create() function. You can change the create() function as follows:

    static std::shared_ptr<back> create(boost::asio::io_service* ios) {
        auto p = std::make_shared<back>(ios);
        p->wp = p; // set wp after creation.
        return p;
    }

In addition, create() and process() use template parameters that with &&. They are called as forwarding-reference (universal-reference). It is an idiom called perfect-forwarding. See http://en.cppreference.com/w/cpp/utility/forward

  1. In hand with 2, a better explanation of the purpose of the wp and ios member variables of Sm would be great. What do you mean by using ios pointer to intentionally meet copy constructor? I furthermore do not see ios being set anywhere but in the constructor Sm(boost::asio::io_service* ios) : ios(ios) {}, which it seems that you never call?

Boost.MSM doesn't support forwarding-reference, so far. I wrote a pull request See https://github.com/boostorg/msm/pull/8

So forwarding-reference invokes copy-constructor in the Boost.MSM. That is the reason I choose the pointer of boost::asio::io_service. However, it is not an essential point of the original question. If I don't use forwarding-reference, I can use the reference types in Sm. So I update the code as follows:

    static std::shared_ptr<back> create(boost::asio::io_service& ios) {
        auto p = std::make_shared<back>(std::ref(ios));
        p->wp = p; // set wp after creation.
        return p;
    }

std::ref is not for make_shared. It is for Boost.MSM. Boost.MSM's constructor requires specifying reference or not due to lack of the forwarding reference support.

  1. Inside the State1_ front-end, you have three BOOST_STATIC_ASSERT calls in the three on_entry methods. What are these doing?

It does nothing in the run-time. Just cheking the type of Fsm at the compile-time. Sometimes I got confused the type of Fsm. I guess the readers also might get confused, so I leave it in the code.

  1. In the main() function, I was able to delete the line auto t = std::make_shared(ios); without changing the behaviour - was it redundant?

Aha, I forgot erase it. I update the code.

Here is the updated code:

#include <iostream>

#include <boost/asio.hpp>

#include <boost/msm/back/state_machine.hpp>
#include <boost/msm/front/state_machine_def.hpp>
#include <boost/msm/front/functor_row.hpp>

namespace msm = boost::msm;
namespace msmf = boost::msm::front;
namespace mpl = boost::mpl;


// ----- State machine
struct Sm : msmf::state_machine_def<Sm> {
    using back = msm::back::state_machine<Sm>;

    static std::shared_ptr<back> create(boost::asio::io_service& ios) {
        auto p = std::make_shared<back>(std::ref(ios));
        p->wp = p; // set wp after creation.
        return p;
    }

    template <typename Ev>
    void process(Ev&& ev) {
        // process_event via backend weak_ptr
        wp.lock()->process_event(std::forward<Ev>(ev));
    }

    // ----- Events
    struct EvSetParent {};
    struct After2 {};
    struct After5 {};

    Sm(boost::asio::io_service& ios):ios(ios) {}
    struct State1_:msmf::state_machine_def<State1_> {
        template <class Event,class Fsm>
        void on_entry(Event const&, Fsm& f) const {
            BOOST_STATIC_ASSERT((boost::is_convertible<Fsm, Sm>::value));
            std::cout << "State1::on_entry()" << std::endl;
            f.process(EvSetParent());
        }

        struct Action {
            template <class Event, class Fsm, class SourceState, class TargetState>
            void operator()(Event const&, Fsm&, SourceState&, TargetState&) const {
                std::cout << "Trying again..." << std::endl;
            }
        };

        struct A:msmf::state<> {
            template <class Event,class Fsm>
            void on_entry(Event const&, Fsm& f) const {
                BOOST_STATIC_ASSERT((boost::is_convertible<Fsm, State1_>::value));
                std::cout << "A::on_entry()" << std::endl;
                auto t = std::make_shared<boost::asio::deadline_timer>(f.parent->ios);
                t->expires_from_now(boost::posix_time::seconds(2));
                t->async_wait([t, &f](boost::system::error_code const) {
                        f.parent->process(After2());
                    }
                );
            }
        };

        struct B:msmf::state<> {
            template <class Event,class Fsm>
            void on_entry(Event const&, Fsm& f) const {
                BOOST_STATIC_ASSERT((boost::is_convertible<Fsm, State1_>::value));
                std::cout << "B::on_entry()" << std::endl;
                auto t = std::make_shared<boost::asio::deadline_timer>(f.parent->ios);
                t->expires_from_now(boost::posix_time::seconds(5));
                t->async_wait([t, &f](boost::system::error_code const) {
                        f.parent->process(After5());
                    }
                );
            }
        };

        // Set initial state
        typedef mpl::vector<A, B> initial_state;
        // Transition table
        struct transition_table:mpl::vector<
            //          Start  Event   Next       Action      Guard
            msmf::Row < A,     After2, A,         Action,     msmf::none >,
            msmf::Row < B,     After5, B,         Action,     msmf::none >
        > {};

        Sm* parent;
    };

    typedef msm::back::state_machine<State1_> State1;

    // Set initial state
    typedef State1 initial_state;

    struct ActSetParent {
        template <class Event, class Fsm, class SourceState, class TargetState>
        void operator()(Event const&, Fsm& f, SourceState& s, TargetState&) const {
                std::cout << "ActSetIos" << std::endl;
                s.parent = &f; // set parent state machine to use process() in A and B.
        }
    };
    // Transition table
    struct transition_table:mpl::vector<
        //          Start   Event        Next        Action        Guard
        msmf::Row < State1, EvSetParent, msmf::none, ActSetParent, msmf::none >
    > {};

    // front-end can access to back-end via wp.
    std::weak_ptr<back> wp;

    boost::asio::io_service& ios;
};


int main() {
    boost::asio::io_service ios;

    auto sm = Sm::create(ios);

    ios.post(
        [&]{
            sm->start();
        }
    );

    ios.run();
}
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!