状态机实现

3
我有一个如下所述的状态机。
我们可以从两个起始状态之一开始,但必须触发握手的所有4个状态。然后,我们可以传输有效数据或接收有效数据。最后,我们返回到原始起始状态。
握手:
-> StartingState1 -> FinalState1 -> StartingState2 -> FinalState2
-> StartingState2 -> FinalState2 -> StartingState1 -> FinalState1
有效数据传输:
-> SendPayload -> SendEnd -> StartingState?
-> ReceivePayload -> ReceiveEnd -> StartingState?
以下代码表示我的当前架构。不幸的是,在每个过程结束时,我没有足够的信息来知道下一个应该触发的状态。
有人有什么建议可以根据我的要求改进这个架构吗?
谢谢, PaulH
class MyMachine;
class Payload;

class IState
{
    MyMachine* context_;

    IState( MyMachine* context ) : context_( context) {};

    virtual void Consume( byte data );

    void ChangeState( IState* state )
    {
        context_->SetState( state );
    }
}

class FinalState1 : IState
{
    void Consume( byte data )
    {
        // Either go to StartingState1, SendPayload, or ReceivePayload.
        // How can I tell from within the context of this state where I 
        // should go?
    }
}

class StartingState1 : IState
{
    void Consume( byte data )
    {
        if ( /*some condition*/ )
        {
            ChangeState( new FinalState1( context_ ) );
        } 
    }
}

class MyMachine
{
    IState* state_;
    Payload* payload_;

    void Start1( Mode mode )
    {
        state_ = new StartingState1( this );
    }

    void Start2( Mode mode )
    {
        state_ = new StartingState2( this );
    }

    void Consume( byte data )
    {
        state_->Consume( data );
    }

    void SetPayload( const Payload* payload )
    {
        payload_ = payload;
    }

    const Payload* GetPayload()
    {
        return payload_;
    }

    void SetState( State* state )
    {
        delete state_;
        state_ = state;
    }
}

// get a byte of data from some source
byte GetData();

void main()
{
    MyMachine machine;
    Payload payload;
    machine.SetPayload( payload );
    machine.Start1( Mode::SendPayload );

    // could also call:
    // machine.Start1( Mode::ReceivePayload );
    // machine.Start2( Mode::SendPayload );
    // machine.Start2( Mode::ReceivePayload );

    for(;;)
    {
        machine.Consume( GetData() );
    }
}

这听起来像是一个纯粹的FSM设计问题,而不是代码问题。 - Hamish Grubijan
@Hamish - 你有关于改进FSM设计的建议吗? - PaulH
我建议您研究状态机语言和编译器。我的直觉告诉我,有一些工具可以根据状态机描述生成框架或模板。 - Thomas Matthews
2
http://qfsm.sourceforge.net/ 是一个有用的工具,用于设计和测试有限状态机,并且可以将构建好的有限状态机导出到各种输出格式,包括C++代码。即使您只是使用输出的代码作为提示并手动编辑它,它也应该显著地帮助设计。 - Justin Smith
5个回答

5

你所拥有的并不能完全代表你的系统可能的状态,但很容易进行转换以达到这个目的。你需要额外的状态来表示处于状态1和未曾处于状态2之间的差异,以及同时处于状态1和曾经处于状态2之间的差异(对于状态2同样如此)。因此你需要:

S1 S2 F1 F2 S12 F12 S21 F21
SP SE
RP RE

使用过渡效果

S1 --> F1
F1 --> S12
S12 --> F12
F12 --> SP or F12 --> RP

S2 --> F2
F2 --> S21
S21 --> F21
F21 --> SP or F21 --> RP

SP --> SE
RP --> RE
SE --> S1 or SE --> S2
RE --> S1 or RE --> S2

关键的区别在于引入了新状态 S12, F12, S21F21。在实现上,您几乎可以从 S2 推导出 S12,从 F2 推导出 F12,从 S1 推导出 S21,从 F2 推导出 F21,并覆盖转换函数以进入正确的状态。
(抱歉缩写所有状态)。

我认为你的解决方案最适合我的现有架构。 感谢你的建议! - PaulH

3

通常我很喜欢使用boost库。但是,这次我需要在不引入外部库的情况下完成这个任务。 - PaulH
大部分的boost只是头文件。 - Andrew McGregor
boost::statechart 是否是表驱动的? - choxsword

2

这里是使用Thomas建议的方法的示例:

#include <cassert>
#include <iostream>
#include <map>

class Machine;

typedef void (*StateFunctionPtr)(Machine& context);

// State "do" functions
void starting1(Machine& context)        {std::cout << "S1 ";}
void final1(Machine& context)           {std::cout << "F1 ";}
void starting2(Machine& context)        {std::cout << "S2 ";}
void final2(Machine& context)           {std::cout << "F2 ";}
void sendPayload(Machine& context)      {std::cout << "SP ";}
void sendEnd(Machine& context)          {std::cout << "SE ";}
void receivePayload(Machine& context)   {std::cout << "RP ";}
void receiveEnd(Machine& context)       {std::cout << "RE ";}

namespace State
{
    enum Type {start, handshake1, handshake2, handshake3,
        handshake4, xferPayload, endPayload};
};

// Aggregate of state, "mode" variables, and events.
struct StateKey
{
    // Needed for use as map key
    bool operator<(const StateKey& rhs) const
    {
        return
              (state < rhs.state)
        ||  ( (state == rhs.state) && (isReceiving < rhs.isReceiving) )
        ||  ( (state == rhs.state) && (isReceiving == rhs.isReceiving)
                && (startsAt2 < rhs.startsAt2) );
    }

    bool startsAt2;
    bool isReceiving;
    State::Type state;
};

struct StateEffect
{
    StateFunctionPtr function;  // "do" function
    State::Type newState;       // state to transition to
};

struct StatePair
{
    StateKey key;
    StateEffect effect;
};

const StatePair stateTable[] =
{
    {{0, 0, State::start},       {&starting1,       State::handshake1}},
    {{0, 0, State::handshake1},  {&final1,          State::handshake2}},
    {{0, 0, State::handshake2},  {&starting2,       State::handshake3}},
    {{0, 0, State::handshake3},  {&final2,          State::handshake4}},
    {{0, 0, State::handshake4},  {&sendPayload,     State::xferPayload}},
    {{0, 0, State::xferPayload}, {&sendEnd,         State::endPayload}},
    {{0, 0, State::endPayload},  {&starting1,       State::handshake1}},

    {{0, 1, State::start},       {&starting1,       State::handshake1}},
    {{0, 1, State::handshake1},  {&final1,          State::handshake2}},
    {{0, 1, State::handshake2},  {&starting2,       State::handshake3}},
    {{0, 1, State::handshake3},  {&final2,          State::handshake4}},
    {{0, 1, State::handshake4},  {&receivePayload,  State::xferPayload}},
    {{0, 1, State::xferPayload}, {&receiveEnd,      State::endPayload}},
    {{0, 1, State::endPayload},  {&starting1,       State::handshake1}},

    {{1, 0, State::start},       {&starting2,       State::handshake1}},
    {{1, 0, State::handshake1},  {&final2,          State::handshake2}},
    {{1, 0, State::handshake2},  {&starting1,       State::handshake3}},
    {{1, 0, State::handshake3},  {&final1,          State::handshake4}},
    {{1, 0, State::handshake4},  {&sendPayload,     State::xferPayload}},
    {{1, 0, State::xferPayload}, {&sendEnd,         State::endPayload}},
    {{1, 0, State::endPayload},  {&starting2,       State::handshake1}},

    {{1, 1, State::start},       {&starting2,       State::handshake1}},
    {{1, 1, State::handshake1},  {&final2,          State::handshake2}},
    {{1, 1, State::handshake2},  {&starting1,       State::handshake3}},
    {{1, 1, State::handshake3},  {&final1,          State::handshake4}},
    {{1, 1, State::handshake4},  {&receivePayload,  State::xferPayload}},
    {{1, 1, State::xferPayload}, {&receiveEnd,      State::endPayload}},
    {{1, 1, State::endPayload},  {&starting2,       State::handshake1}}
};


class Machine
{
public:
    Machine()
    {
        // Initialize state chart map from constant state table
        const size_t tableSize = sizeof(stateTable) / sizeof(stateTable[0]);
        for (size_t row=0; row<tableSize; ++row)
        {
            stateChart_[stateTable[row].key] = stateTable[row].effect;
        }
    }

    // If startsAt2==true, then FSM will start with starting2 handshake function
    void reset(bool startsAt2, bool isReceiving)
    {
        stateKey_.startsAt2 = startsAt2;
        stateKey_.isReceiving = isReceiving;
        stateKey_.state = State::start;
    }

    void step()
    {
        StateChart::const_iterator iter = stateChart_.find(stateKey_);
        assert(iter != stateChart_.end());
        const StateEffect& effect = iter->second;
        effect.function(*this);
        stateKey_.state = effect.newState;
    }

private:
    typedef std::map<StateKey, StateEffect> StateChart;
    StateChart stateChart_;
    StateKey stateKey_;
};

int main()
{
    Machine machine;
    machine.reset(true, false);
    for (int i=0; i<20; ++i)
    {
        machine.step();
    }
}

它在我的机器上编译并工作。您可能希望添加以下功能:

  • StateEffect中的入口/出口函数
  • StateKey中的事件“触发器”
  • 泛化为一个模板。

添加足够通用的功能,它将开始类似于Boost.StateChart的想法。


2

我建议从函数对象或函数指针的角度进行设计。

一个简单的状态机可以使用数组或std::map来实现。使用当前状态作为索引,检索新状态或指向状态函数的指针。

更复杂的状态机基于转换事件从一个状态到另一个状态。简单实现这需要一个“嵌套”的数组。使用转换作为索引访问状态的转换表,返回处理此转换的函数的函数指针。

有不同的数据结构可用,这取决于状态机的复杂性。

一个好的想法是使用表驱动状态机。这允许将引擎编写和测试一次。更改状态机涉及更改表中的数据。该表可以存在于可执行文件之外,这意味着可执行文件无需更改。通过使用动态库可以扩展此概念,减少更改可执行文件的需要。

这只是我的建议,可能不完全正确(摘自Dennis Miller)。


你有这种状态机的示例链接吗? - PaulH

0

您可以使用Petri网模型来建立状态机。这样可以定义非常简单和非常复杂的状态机。 要实现指定的状态机/Petri网,您可以使用像PTN Engine这样的引擎。

它允许您在Petri网构造函数中声明式地定义整个状态机。您可以集成自己的函数,在到达给定状态时调用它们,以及触发状态更改的函数。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接