转向现代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 成员变量的类:

  • 默认复制构造函数被删除(需要手动实现)
  • 默认移动构造函数可以 正常工作
相关推荐
坚果派·白晓明43 分钟前
[鸿蒙PC三方库移植适配] 使用 AtomCode + Skills 自动完成Protobuf鸿蒙化适配
c语言·c++·华为·harmonyos
原来是猿1 小时前
深入理解 C++ unordered_map 与 unordered_set
开发语言·c++
满天星83035771 小时前
【Qt】信号和槽 (一)(概述和基本使用)
开发语言·c++·qt
努力的章鱼bro1 小时前
CUDA编程模型
c++·cuda
l1t1 小时前
DeepSeek总结的 waddler,一个 Go 语言编写的从 YAML 文件运行的 ETL 管道
开发语言·golang·etl
FlyWIHTSKY1 小时前
React 19 + Next.js 16(App Router)项目中集成 MSW
开发语言·javascript·vue.js
Mr.Daozhi1 小时前
跨境电商选品完整流水线:Google Trends筛词+Meta广告分析,CLI工具设计实战
开发语言·爬虫·python·跨境电商·工具链·选品
多彩电脑1 小时前
Swift里字符串的索引
开发语言·swift
SoftLipaRZC1 小时前
C语言预处理详解:从宏定义到条件编译
c语言·开发语言