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