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

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循环是确保线程安全的关键机制。

相关文章:

  • 中国小说网站策划与建设广告投放都有哪些平台
  • 网站内容建设总结山西网络推广专业
  • 网站制作在哪里找网络营销方案策划案例
  • 设计本官方网站电脑版重庆森林经典台词梁朝伟
  • 保洁公司做网站有什么作用山东免费网络推广工具
  • 日报社网站平台建设项目标题关键词优化技巧
  • Python应用“面向对象”小练习
  • OpenOCD 与 PlatformIO
  • 010501上传下载_反弹shell-渗透命令-基础入门-网络安全
  • C++ 继承的相关内容 基类和派生类 默认成员函数的区别等问题
  • 机器学习k近邻,高斯朴素贝叶斯分类器
  • 将 Docker 镜像从服务器A迁移到服务器B的方法
  • 【Axure结合Echarts绘制图表】
  • “安康杯”安全生产知识竞赛活动流程方案
  • ATPrompt方法:属性嵌入的文本提示学习
  • 本周 edu教育邮箱注册可行方案
  • 车载通信网络 --- 传统车载网络及其发展
  • 【C++高级主题】异常处理(四):auto_ptr类
  • C++异步日志系统
  • 力扣 155.最小栈
  • sqli-labs第二十七关——Trick with selectunion
  • Queue 与 Deque 有什么区别?
  • 人工智能第一币AISPF,首发BitMart交易所
  • C++笔记-哈希表
  • etcd之etcd curl命令(七)
  • 《反事实棱镜:折射因果表征学习的深层逻辑》