转向现代C++——保证const成员函数的线程安全性

文章目录

保证 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

⚠️**两个问题**:

  1. 重复计算 :线程A执行完第一步但尚未执行第二步时,线程B看到 cacheValid == false,也进入计算分支,导致重复计算。
  2. 读到中间状态 :如果交换步骤顺序(先 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
};

核心要点与补充

  1. 确保 const 成员函数线程安全 ,除非你确定 它们永远不会在并发上下文中被使用。
    • 标准库中 std::vector::size()std::string::length()const 成员函数都是线程安全的------你的类也应如此。
  2. std::atomicstd::mutex 性能更好,但仅适用于单个变量或内存位置的同步操作。
  3. std::mutex 适用于需要将多个变量作为一个整体进行同步的场景。
  4. 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 成员变量的类:

  • 默认复制构造函数被删除(需要手动实现)
  • 默认移动构造函数可以 正常工作
相关推荐
郝学胜_神的一滴20 小时前
CMake 30:循环语法全解|foreach_while双循环精讲、迭代技巧与实战避坑指南
c++·cmake
卷无止境3 天前
C++ 的Eigen 库全解析
c++
卷无止境3 天前
现代 C++特性大盘点:一门脱胎换骨的老语言
c++·后端
郝学胜_神的一滴3 天前
CMake 27:缓存变量的特性、语法、类型与实操全解
c++·cmake
博客18005 天前
酷宝的使用方法,超好用的免费界面库,C++、MFC可用
c++·mfc·界面库·库来帮·酷宝
郝学胜_神的一滴5 天前
CMake 026:属性体系精讲、四大作用域全解 & 实战代码落地
c++·cmake
众少成多积小致巨6 天前
JNI (Java Native Interface) 技术手册中文参考指南
android·java·c++
clint45610 天前
C++进阶(1)——前景提要
c++
夜悊10 天前
C++代码示例:进制数简单生成工具
c++
郝学胜_神的一滴10 天前
CMake 021: IF 条件判据详诠
c++·cmake