文章目录
保证 const 成员函数的线程安全性
const 的线程安全隐患
const 成员函数在概念上表示"只读"操作,通常被认为是天然线程安全的。但实际上,当 const 成员函数内部使用了 mutable 成员变量(例如缓存计算结果、统计调用次数等)时,线程安全问题就会暴露出来。
cpp
class Polynomial {
public:
using RootsType = std::vector<double>;
RootsType roots() const {
if (!rootsAreValid) { // 如果缓存不可用
// ... 计算根,存入 rootVals(耗时操作)
rootsAreValid = true;
}
return rootVals;
}
private:
mutable bool rootsAreValid{ false };
mutable RootsType rootVals{};
};
⚠️**问题**:当两个线程同时调用 p.roots() 时,它们可能同时进入 if 分支,同时修改 rootsAreValid 和 rootVals`,导致**数据竞争(data race)**,程序行为未定义。
多个线程在没有同步机制的情况下同时读写同一内存位置 → 数据竞争 → 未定义行为。
解决方案
std::mutex(互斥量)
cpp
class Polynomial {
public:
using RootsType = std::vector<double>;
RootsType roots() const {
std::lock_guard<std::mutex> g(m); // 锁定互斥量
if (!rootsAreValid) {
// ... 计算并缓存根值
rootsAreValid = true;
}
return rootVals;
} // g析构,自动解锁
private:
mutable std::mutex m;
mutable bool rootsAreValid{ false };
mutable RootsType rootVals{};
};
:::danger
🔧**注意事项**
std::mutex** 既不可复制也不可移动**,因此包含它的类也会失去复制和移动能力。std::lock_guard是 RAII 风格的锁管理器,构造时加锁,析构时自动解锁(例外安全)。- 即使函数内部抛异常,
lock_guard也会在栈展开时自动释放锁,不会造成死锁。
:::
std::atomic(原子变量)
当只需同步单个变量或内存位置 时,std::atomic 是更轻量的选择:
cpp
class Point {
public:
double distanceFromOrigin() const noexcept {
++callCount; // 原子递增,无锁
return std::sqrt((x * x) + (y * y));
}
private:
mutable std::atomic<unsigned> callCount{ 0 };
double x, y;
};
| 特性 | std::atomic |
std::mutex |
|---|---|---|
| 性能开销 | 低(通常无锁) | 高(系统调用) |
| 适用场景 | 单个变量/内存位置 | 多个变量的整体同步 |
| 可复制/可移动 | 不可复制,可移动 | 既不可复制也不可移动 |
std::atomic 的陷阱
当需要同步两个或更多变量作为一个整体 时,std::atomic 不够用,会引入竞争条件。
cpp
class Widget {
public:
int magicValue() const {
if (cacheValid) {
return cachedValue;
} else {
auto val1 = expensiveComputation1();
auto val2 = expensiveComputation2();
cachedValue = val1 + val2; // 第一步
cacheValid = true; // 第二步 --- 非原子!
return cachedValue;
}
}
private:
mutable std::atomic<bool> cacheValid{ false };
mutable std::atomic<int> cachedValue;
};
:::danger
⚠️**两个问题**:
- 重复计算 :线程A执行完第一步但尚未执行第二步时,线程B看到
cacheValid == false,也进入计算分支,导致重复计算。 - 读到中间状态 :如果交换步骤顺序(先
cacheValid = true再赋值cachedValue),线程B可能在cachedValue写完之前就看到cacheValid == true,读到未完全初始化的值。
:::
当多个变量需要作为一个整体 同步时,回退到 <font style="color:#1DC0C9;">std::mutex</font>:
cpp
class Widget {
public:
int magicValue() const {
std::lock_guard<std::mutex> g(m);
if (cacheValid) {
return cachedValue;
} else {
auto val1 = expensiveComputation1();
auto val2 = expensiveComputation2();
cachedValue = val1 + val2;
cacheValid = true;
return cachedValue;
}
}
private:
mutable std::mutex m;
mutable bool cacheValid{ false };
mutable int cachedValue; // 不再需要 atomic
};
核心要点与补充
- 确保
const成员函数线程安全 ,除非你确定 它们永远不会在并发上下文中被使用。- 标准库中
std::vector::size()、std::string::length()等const成员函数都是线程安全的------你的类也应如此。
- 标准库中
std::atomic比std::mutex性能更好,但仅适用于单个变量或内存位置的同步操作。std::mutex适用于需要将多个变量作为一个整体进行同步的场景。std::mutex(既不可复制也不可移动)和std::atomic(不可复制、可移动)都会使包含它们的类失去复制能力;如果类需要被复制,需要自行实现复制构造函数来初始化新的 mutex/atomic。
std::atomic 的移动支持
cpp
`// std::atomic 可移动但不可复制
std::atomic<int> a{ 42 };
std::atomic<int> b = a; // ❌ 编译错误:copy ctor = delete
std::atomic<int> c = std::move(a); // ✅ 通过移动构造`
这意味着包含 std::atomic 成员变量的类:
- 默认复制构造函数被删除(需要手动实现)
- 默认移动构造函数可以 正常工作