C++中可重入且线程安全的FSM核心是状态迁移逻辑与上下文数据解耦,每个线程持独立Context实例;FSM自身无状态、无成员变量,仅通过引用接收上下文并纯函数式处理事件。

直接说结论:C++ 里实现可重入且线程安全的 FSM,核心不是“加个 std::mutex 就完事”,而是把状态迁移逻辑与上下文数据彻底解耦,并让每个线程持有独立的状态上下文实例。
为什么 std::mutex 保护整个 FSM 类通常不够用
常见错误是给 process_event() 加锁,以为这样就线程安全了。但问题在于:
- 若 FSM 内部缓存了共享的上下文指针(比如指向某个全局
Config或Session),锁只防住了状态跳转,没防住上下文被多线程并发修改 - 可重入性被破坏:同一线程在回调中再次调用
process_event()可能死锁(递归锁虽可解,但掩盖设计缺陷) - 性能瓶颈:所有事件排队串行处理,哪怕它们本可作用于完全无关的状态实例
用“状态上下文”替代“状态变量”:每个线程/任务一个 Context 实例
关键转变:FSM 本身不存状态,只存转移表和动作逻辑;真实状态(如当前状态 ID、计数器、临时缓冲区)全由外部传入的 Context& 承载。
示例结构:
struct TcpSessionContext {
enum State { IDLE, CONNECTING, ESTABLISHED, CLOSING };
State current_state = IDLE;
int retry_count = 0;
std::vector<uint8_t> rx_buffer;
};
class TcpFsm {
public:
void process_event(const ConnectEvent&, TcpSessionContext& ctx);
void process_event(const DataReceivedEvent&, TcpSessionContext& ctx);
void process_event(const TimeoutEvent&, TcpSessionContext& ctx);
private:
// 无成员变量存储状态 —— 纯函数式行为
};</uint8_t>
这样,线程 A 处理 session_1,线程 B 处理 session_2,各自传入自己的 TcpSessionContext 实例,天然隔离,无需锁。
异构上下文怎么统一接入?用 std::variant + 访问者模式
不同业务状态机需要不同上下文类型(HttpCtx、UdpCtx、AuthCtx),但又希望复用同一套 FSM 调度逻辑。这时避免用 void* 或基类指针——它们破坏类型安全且无法静态分发。
推荐方案:
- 定义统一的上下文变体:
using FsmContext = std::variant - FSM 的
process_event()接收FsmContext&,内部用std::visit分发到对应特化处理函数 - 每个特化函数签名一致:
void handle_connect(TcpSessionContext&)、void handle_connect(HttpSessionContext&),编译期绑定,零开销
好处:运行时支持多类型,编译期仍保证类型安全,且无虚函数调用开销。
可重入的关键:禁止 FSM 内部持有 this 指针或静态状态
以下写法会破坏可重入性:
- 在
on_enter_ESTABLISHED()回调里调用this->start_timer(),而start_timer()内部又调用了process_event(TimeoutEvent{}) - 使用
static std::queue缓存待处理事件 - 在状态动作中修改 FSM 类的
member_data_(哪怕加了锁)
正确做法:
- 所有副作用(定时器、IO 提交、日志)都通过回调函数对象传入,由调用方控制生命周期
- 事件队列放在调用方栈/堆上(如
std::vector),FSM 只负责消费传入的单个事件pending_events - 状态迁移结果(新状态、是否需后续动作)以返回值形式给出,不隐式改变内部状态
真正难的不是写锁,而是把“谁拥有状态”、“谁触发副作用”、“谁决定下一步”这三件事拆清楚。一旦上下文外置、事件驱动、无内部可变状态,线程安全和可重入性就变成自然结果,而不是靠补丁堆出来的。
C++免费学习笔记(深入):立即使用
在学习笔记中,你将探索 C++ 的入门与实战技巧!











