const 成员函数承诺 "不修改对象核心逻辑状态",但常因mutable修饰的辅助变量(如多项式根的缓存rootVals、缓存有效标记rootsAreValid),在执行时修改这些变量;
多线程同时调用该 const 函数时,会出现 "不同线程读写同一块内存" 的数据竞争,导致程序行为未定义(即使逻辑上是 "读操作")。
线程安全的解决方案
1. 基础方案:使用std::mutex
-
适用场景:需要同步多个变量 / 多个操作(如缓存值 + 缓存有效标记),保证操作的原子性;
-
实现要点:
- 将
std::mutex声明为mutable(因const成员函数中mutex被视为const对象,而锁 / 解锁操作会修改mutex状态); - 用
std::lock_guard自动加锁 / 解锁,避免手动解锁遗漏;
- 将
-
副作用:
std::mutex不可拷贝 / 移动,导致包含它的类也不可拷贝 / 移动; -
示例:多项式根缓存的线程安全实现(加锁后保证缓存计算 / 赋值的原子性)。
2. 轻量方案:使用std::atomic
-
适用场景:仅需同步单个变量 (如函数调用次数统计),开销比
mutex小; -
实现要点:将统计变量声明为
std::atomic<类型>且mutable,其操作是原子的(不可分割),避免数据竞争; -
副作用:
std::atomic同样不可拷贝 / 移动,导致包含它的类不可拷贝 / 移动; -
坑点:绝对不能用于多个变量的同步(如缓存值 + 缓存有效标记):
- 先赋值缓存值、后标记有效:会导致多线程重复计算(违背缓存初衷);
- 先标记有效、后赋值缓存值:会导致线程读取到未赋值的脏数据。
| 方案 | 适用场景 | 优势 | 限制 |
|---|---|---|---|
std::mutex |
多个变量 / 操作的同步 | 保证操作原子性 | 开销略大、类不可拷贝 / 移动 |
std::atomic |
单个变量的同步 | 开销小、操作原子化 | 无法同步多个变量 / 操作 |
例外情况:无需保证线程安全的场景
若能绝对保证 const 成员函数永远不会在多线程环境下调用 (如仅用于单线程独占的类),可省略 mutex/atomic,避免资源开销和类拷贝 / 移动的限制;但这种场景越来越少见,优先保证线程安全。
总结
- 保证
const成员函数的线程安全性,除非可以确信它们不会用在并发语境中。 - 运用
std::atomic类型的变量会比运用互斥量提供更好的性能,但前者仅适用对单个变量或内存区域的操作。