std::any 不是线程安全的上下文载体,因其无内置同步机制,并发读写同一实例会导致未定义行为;必须由上层用互斥锁等手段保证访问串行化。

为什么 std::any 不是线程安全的上下文载体
std::any 本身不提供任何线程同步机制,它的 has_value()、type()、emplace<t>()</t> 等操作在多线程并发读写同一实例时会引发未定义行为。常见错误现象包括:状态机跳转时 std::bad_any_cast 频发、上下文数据被意外覆盖、甚至 std::any 内部指针损坏导致崩溃。
这不是 std::any 的缺陷,而是设计使然——它定位为类型擦除容器,不是并发原语。若直接裸用 std::any 存储状态上下文(如 std::any context;),必须由上层保证访问互斥。
- 所有对同一个
std::any实例的读/写/修改操作,必须串行化(例如加std::mutex) - 避免在状态转移回调中直接修改该
std::any,除非回调已处于锁保护范围内 - 不要依赖
std::any的移动构造或赋值来“隐式同步”——它们不带锁
可重入 FSM 的核心约束:状态转移函数不能持有共享状态锁
可重入 ≠ 可并发。一个可重入的状态机允许同一线程多次嵌套调用(比如状态 A 的处理函数内部触发一次 transit_to(B)),但若在锁内调用 transit_to,就可能因递归加锁或死锁失败(尤其使用 std::mutex 而非 std::recursive_mutex)。
更关键的是:状态转移逻辑本身应无副作用、不依赖外部可变状态,否则“重入”就会破坏一致性。
立即学习“C++免费学习笔记(深入)”;
- 把状态转移判定(guard)和上下文变更(action)拆开:guard 只读取
std::any,action 在锁外纯函数式生成新上下文 - 实际更新
std::any和切换current_state必须原子完成,推荐用std::atomic<:size_t></:size_t>管理状态 ID,并配合一次 mutex 加锁写入 - 避免在 guard 中做耗时操作(如文件 I/O、网络等待),否则会阻塞整个状态机线程
如何封装线程安全的 any_context 类
直接继承或包装 std::any 不够,需要显式控制访问路径。一个最小可行封装是暴露三个线程安全接口:get_or<t>()</t>(带默认值读取)、set<t>(const T&)</t>(类型安全写入)、reset()(清空)。内部用 mutable std::shared_mutex 区分读写锁粒度。
class any_context {
mutable std::shared_mutex mtx_;
std::any data_;
public:
template<typename T>
T get_or(const T& def) const {
std::shared_lock lock(mtx_);
if (data_.type() == typeid(T)) {
return std::any_cast<T>(data_);
}
return def;
}
template<typename T>
void set(const T& value) {
std::unique_lock lock(mtx_);
data_ = value;
}
void reset() {
std::unique_lock lock(mtx_);
data_.reset();
}
};
注意:这里不用 std::mutex 是为了支持并发读;但 set() 和 reset() 仍需排他写,否则多个线程同时 set<int></int> 和 set<:string></:string> 会导致 data_ 内部状态撕裂。
FSM 主体类中避免虚函数 + 锁的性能陷阱
很多实现把每个状态抽象为虚函数对象(struct State { virtual void on_enter(any_context&) = 0; }),再用 std::unique_ptr<state></state> 持有当前状态。这在高频率状态跳转场景下,会因虚表查找 + 动态内存访问 + 锁竞争三重开销明显拖慢吞吐。
更轻量的做法是用 std::array<:function>, N></:function> 静态存下各状态的 enter/exit/handle 行为,状态 ID 即数组索引。所有跳转通过整数 ID + 原子 load/store 控制,锁只用于保护 any_context 和状态 ID 本身的更新。
- 状态数量固定且可预估时,优先用数组索引代替虚函数或多态指针
- 把
std::function绑定到具体 lambda 或静态函数,避免捕获大对象(尤其是捕获*this)造成额外分配 - 若必须支持运行时注册状态行为,请确保注册过程是单线程的(如仅在初始化阶段),避免锁竞争点扩散
真正难的不是让状态机“看起来能并发”,而是厘清哪些操作必须原子、哪些可以放宽、哪些根本不该放在线程临界区里。哪怕用了 std::any,上下文数据的生命周期、转移时机、以及谁负责析构——这些细节一旦错位,线程安全就只是幻觉。


















