当前位置: 首页 > web >正文

QAtomicInt原子变量的CAS(Compare And Swap)写法与优缺点

    在Qt的多线程编程中,有时用到原子变量类型QAtomicInt,与C++原生的std::atomic类似,优点是:可以确保对象只有一个;缺点是:对象的状态,在不同的内存模型/线程中,有不同的状态的,需要做同步操作。
    原子变量的同步操作,既可以借助锁、信号量、临界区等额外变量来实现,也可以原子变量自身的CAS方法,来做同步。
    这里介绍原子变量的CAS写法,比如,下面第1节代码A,是一个无锁队列,采用QAtomiInt+QSemaphore实现。其中,入队函数是put(const Pancake& pancake), 出队函数是take()。
    这里的put(const Pancake& pancake)函数,采用了CAS方法,实现m_tail变量,在多个线程里的状态同步,即在线程ThreadA里看到m_tail值,与线程ThreadB里看到的m_tail值是一致的。

1 无锁队列-- 代码A

    下面是一个C++编写的无锁队列。

// 1)煎饼类
class Pancake {
public:Pancake(int id) : m_id(id) {}int getId() const { return m_id; }private:int m_id;
};// 2) 无锁队列
class PancakeTray_LockFree_WithSemaphore {
public:PancakeTray_LockFree_WithSemaphore(int capacity = 5) : m_capacity(capacity + 1), m_buffer(capacity + 1) {m_head = 0;m_tail = 0;m_notFull = new QSemaphore(capacity);   // 控制容量m_notEmpty = new QSemaphore(0);         // 控制可用项数量}~PancakeTray_LockFree_WithSemaphore() {delete m_notFull;delete m_notEmpty;}void put(const Pancake& pancake) {m_notFull->acquire();  // 等待空位,避免忙等// 无锁获取写入位置int currentTail;int nextTail;do {currentTail = m_tail;nextTail = (currentTail + 1) % m_capacity;} while (!m_tail.testAndSetOrdered(currentTail, nextTail));// 写入数据m_buffer[currentTail] = pancake;qDebug() << QString("师傅制作了煎饼#%1,托盘上现有%2个煎饼").arg(pancake.getId()).arg(getCount());m_notEmpty->release(); // 通知有新项可用}Pancake take() {m_notEmpty->acquire(); // 等待可用项,避免忙等// 无锁获取读取位置int currentHead;int nextHead;do {currentHead = m_head;nextHead = (currentHead + 1) % m_capacity;} while (!m_head.testAndSetOrdered(currentHead, nextHead));// 读取数据Pancake pancake = m_buffer[currentHead];m_notFull->release();  // 通知有新空位return pancake;}int getCount() const {int head = m_head;int tail = m_tail;return (tail - head + m_capacity) % m_capacity;}private:QVector<Pancake> m_buffer;int m_capacity;QAtomicInt m_head;QAtomicInt m_tail;QSemaphore* m_notFull;QSemaphore* m_notEmpty;
};

2 CAS方法

    我们已经知道,put()函数,属于入队操作,其代码如下:

    void put(const Pancake& pancake) {m_notFull->acquire();  // 等待空位,避免忙等// 无锁获取写入位置int currentTail;int nextTail;do {currentTail = m_tail;nextTail = (currentTail + 1) % m_capacity;} while (!m_tail.testAndSetOrdered(currentTail, nextTail));// 写入数据m_buffer[currentTail] = pancake;qDebug() << QString("师傅制作了煎饼#%1,托盘上现有%2个煎饼").arg(pancake.getId()).arg(getCount());m_notEmpty->release(); // 通知有新项可用}

    那么,put()函数里,“do{} while(m_tail.testAndSetOrdered(currentTail, nextTail))” 的作用是啥?

    这是一个很好的问题!下面,详细解释这个do-while循环的作用和原理。

2.1 问题分析

    这段代码的作用是原子性地获取并更新写入位置,它解决了多线程环境下的竞态条件问题。

do {currentTail = m_tail;                    // 1. 读取当前尾指针nextTail = (currentTail + 1) % m_capacity; // 2. 计算下一个位置
} while (!m_tail.testAndSetOrdered(currentTail, nextTail)); // 3. 原子性比较并交换

2.2 状态更新,为什么需要循环?

    由于是多线程操作这个无锁队列,即至少有2个以上线程,来执行put()函数。为了描述方便,这里只考虑2个线程的情况:线程A、线程B;
    场景如下:

初始状态:m_tail = 2线程A                    线程B
------                   ------
currentTail = 2          currentTail = 2     ← 都读到相同值
nextTail = 3             nextTail = 3        ← 都计算出相同的下一位置testAndSetOrdered(2,3)   testAndSetOrdered(2,3)
返回 true ✓              返回 false ✗        ← 只有一个能成功!

2.3 testAndSetOrdered()函数的工作原理

bool testAndSetOrdered(int expectedValue, int newValue)

    testAndSetOrdered()函数,执行一个原子操作,即要么操作成功,要么操作失败,
    若操作成功,则更新值;若操作失败,则使用原来的值;
具体如下:

  • 1 比较:检查当前值是否等于expectedValue
  • 2 交换:如果相等,设置为newValue
  • 3 返回:返回操作是否成功
    关键特性:整个过程是原子的,不可被打断!

2.4 循环的作用

    在put()函数里的do{}while循环,反复执行testAndSetOrdered()函数,确保多个线程里的m_tail值,都是相同的;若不相同,则m_tail.testAndSetOrdered()返回false,又会进入循环,直到m_tail.testAndSetOrdered()返回true,即m_tail在所有的线程里都更新了。

do {currentTail = m_tail;                    // 重新读取最新值nextTail = (currentTail + 1) % m_capacity;
} while (!m_tail.testAndSetOrdered(currentTail, nextTail)); // 失败就重试

    执行流程如下:

  • 1)第一次尝试:如果没有竞争,直接成功退出;
  • 2)失败重试:如果其他线程修改了m_tail,重新读取最新值并再次尝试;
  • 3)最终成功:直到成功获取到独占的写入位置;

    具体如下:

初始:m_tail = 2时刻1:
线程A: currentTail=2, nextTail=3, testAndSetOrdered(2,3) → 成功,m_tail变为3
线程B: currentTail=2, nextTail=3, testAndSetOrdered(2,3) → 失败(m_tail已经是3了)时刻2:
线程B: 重新读取 currentTail=3, nextTail=4, testAndSetOrdered(3,4) → 成功,m_tail变为4

2.4 为啥不能用原子变量++或–

    如果用原子自身的++:

// ❌ 错误的做法
int pos = m_tail++;  // 不是原子操作!

    会导致如下异常:
-1)数据竞争:多个线程可能得到相同的位置(自减不降,导致数据没有出队);
-2)数据丢失:两个线程写入同一位置(自增覆盖,导致数据没有入队);
-3)缓冲区损坏:指针状态不一致;

2.5 CAS方法的优缺点

    CAS方法的全称:Compare And Swap,具体如下:

  • Compare:比较当前值
  • And:如果相等
  • Swap:交换为新值

优势:
✅ 无需锁,性能高
✅ 避免死锁
✅ 线程安全

缺点:
⚠️ 在高竞争下可能重试多次
⚠️ 实现复杂度较高

    这就是为什么无锁编程既强大又复杂的原因!
    这个do-while循环是确保线程安全的关键机制。

http://www.xdnf.cn/news/9190.html

相关文章:

  • 通信算法之279:数据链/自组网通信设备--MIMO(2T2R)-OFDM系统系列--实际工程应用算法代码--2.OFDM参数设计及帧结构设计
  • 批量无人值守装机(使用cobbler批量安装windows)
  • 用提示词写程序(2),VSCODE+Claude3.5开发edge扩展插件
  • SuperMap GIS基础产品FAQ集锦(20250519)
  • vue + ant-design + xlsx 实现表格数据导出
  • AcrelEMS 3.0智慧能源管理平台:构建企业微电网数智化中枢
  • watchEffect
  • python神经网络学习小结2
  • python时间序列处理
  • 总结:进程和线程的联系和区别
  • 快速上手SHELL脚本常用命令
  • SAP成本核算-事中控制(成本对象控制/成本归集与结算)
  • OpenGL多重渲染
  • 基于Robust Video Matting 使用Unity 实现无绿幕实时人像抠图
  • GJOI 5.24 题解
  • 时空弯曲和测地线浅谈
  • 开卡包的期望
  • 第12次03 :登录状态的保持
  • 一个简单的系统插桩实现​
  • 龙虎榜——20250526
  • C++虚函数和纯虚函数
  • 云原生技术在企业数字化转型中的战略价值与实践路径
  • MySql(三)
  • 高精度装配人形机器人|产品参数详细介绍
  • Day03
  • 架空线路智能云台监控系统介绍
  • 大数据学习(122)-分区与分桶表
  • 【前端】Proxy对象在控制台中惰性求值_vue常见开发问题
  • AI换场景工具:图生生1分钟完成电商商拍
  • Vue 样式穿透(深度选择器)::v-deep