状态机有3个要素:状态,事件,动作。
假如一个对象有3个状态:S1、S2、S3。影响状态的事件有3个:E1、E2、E3。每个状态下收到对应事件的时候,对象的动作为AXY。那么该对象的状态机就可以用如下表格来表示。S1收到事件E1的时候动作为A11,收到事件E2的时候动作为A12,收到事件E3的时候动作为A13,以此类推。
|----|-----|-----|-----|
| | E1 | E2 | E3 |
| S1 | A11 | A12 | A13 |
| S2 | A21 | A22 | A23 |
| S3 | A31 | A32 | A33 |
其中,动作可以是状态发生切换,也可以是其它与业务有关的动作。
1分支逻辑法
分支逻辑法,是最简单 、最直观,也是最常用的一种方法。
如下是分支逻辑法的实现:
if(state == S1) {
if (event == E1) {
A31();
} else if (event == E2) {
A32();
} else {
A33();
}
} else if (state == S2) {
if (event == E1) {
A31();
} else if (event == E2) {
A32();
} else {
A33();
}
} else {
if (event == E1) {
A31();
} else if (event == E2) {
A32();
} else {
A33();
}
}
在linux内核中,tcp有自己的状态机,如下代码,就是在收到tcp报文时,其中的一小段状态机代码,这段代码就是使用分支逻辑法。
int tcp_rcv_state_process(struct sock *sk, struct sk_buff *skb)
{
...
switch (sk->sk_state) {
case TCP_CLOSE:
goto discard;
case TCP_LISTEN:
if (th->ack)
return 1;
if (th->rst)
goto discard;
if (th->syn) {
if (th->fin)
goto discard;
/* It is possible that we process SYN packets from backlog,
* so we need to make sure to disable BH and RCU right there.
*/
rcu_read_lock();
local_bh_disable();
acceptable = icsk->icsk_af_ops->conn_request(sk, skb) >= 0;
local_bh_enable();
rcu_read_unlock();
if (!acceptable)
return 1;
consume_skb(skb);
return 0;
}
goto discard;
case TCP_SYN_SENT:
tp->rx_opt.saw_tstamp = 0;
tcp_mstamp_refresh(tp);
queued = tcp_rcv_synsent_state_process(sk, skb, th);
if (queued >= 0)
return queued;
/* Do step6 onward by hand. */
tcp_urg(sk, skb, th);
__kfree_skb(skb);
tcp_data_snd_check(sk);
return 0;
}
...
}
2查表法
如果状态发生改变,比如增删,或者事件发生改变,比如增加一个事件或者删除一个事件,就需要对代码进行修改。分支逻辑法不满足开闭原则。
查表法使用两个两级map:
①transitionTable
第一级map的key为状态,value是第二级map;第二级map的key是事件类型,value是目标状态。
②actionTable
第一级map的key为状态类型,value是第二级map;第二级map的key是事件类型,value是action。
如下是查表法的示意代码:
cpp
#include <iostream>
#include <map>
#include <functional>
#include <stdexcept> // 用于异常处理
enum class State : int32_t {
S1,
S2,
S3
};
enum class Event : int32_t {
E1,
E2,
E3
};
class StateMachine {
public:
StateMachine(State const& initialState) : state_{initialState} {
// 初始化状态转移表
transitionTable_ = {
{State::S1, {{Event::E1, State::S1}, {Event::E2, State::S2}, {Event::E3, State::S3}}},
{State::S2, {{Event::E1, State::S1}, {Event::E2, State::S3}, {Event::E3, State::S2}}},
{State::S3, {{Event::E1, State::S3}, {Event::E2, State::S2}, {Event::E3, State::S3}}}
};
// 初始化动作表(修复 lambda 捕获和逗号分隔)
actionTable_ = {
{State::S1, {
{Event::E1, [this]() { std::cout << "action11\n"; }},
{Event::E2, [this]() { std::cout << "action12\n"; }},
{Event::E3, [this]() { std::cout << "action13\n"; }}
}},
{State::S2, {
{Event::E1, [this]() { std::cout << "action21\n"; }},
{Event::E2, [this]() { std::cout << "action22\n"; }},
{Event::E3, [this]() { std::cout << "action23\n"; }}
}},
{State::S3, {
{Event::E1, [this]() { std::cout << "action31\n"; }},
{Event::E2, [this]() { std::cout << "action32\n"; }},
{Event::E3, [this]() { std::cout << "action33\n"; }}
}}
};
}
void OnEvent(Event const& event) {
// 检查当前状态和事件是否有效
if (actionTable_.find(state_) == actionTable_.end() ||
actionTable_[state_].find(event) == actionTable_[state_].end()) {
throw std::runtime_error("Invalid state or event!");
}
// 执行动作并更新状态
actionTable_[state_][event](); // 调用函数
state_ = transitionTable_[state_][event];
}
State GetState() {
return state_;
}
private:
State state_;
std::map<State, std::map<Event, State>> transitionTable_;
std::map<State, std::map<Event, std::function<void()>>> actionTable_;
};
int main() {
StateMachine sm{State::S1};
sm.OnEvent(Event::E3); // 修复拼写错误
if (State::S3 != sm.GetState()) {
std::cout << "state " << (int32_t)sm.GetState() << " is not expected\n";
}
sm.OnEvent(Event::E2);
if (State::S2 != sm.GetState()) {
std::cout << "state " << (int32_t)sm.GetState() << " is not expected\n";
}
return 0;
}
查表法相对于分支逻辑法 ,将状态机创建的代码和状态机转换的代码进行了解耦。如果状态有改变或者事件有改变,那么可以修改StateMachine的构造函数,而不需要修改OnEvent函数。
transitionTable_和actionTable_是数据结构,可以看作存储,OnEvent中的代码逻辑可以看作计算逻辑。解耦就是将存储和计算逻辑代码进行分离,将可以修改的部分和不可以修改的部分进行分离。