CAS
CAS(Compare-And-Swap)是一种原子操作,它在多线程编程中用于实现无锁数据结构和同步机制。CAS操作提供了一种机制,允许线程在没有使用传统锁的情况下,安全地更新共享数据。说白了CAS就是对比与交换, 它将一个内存位置上的内容与一个给定的值进行比较,只有它们相同时才将该内存位置的内容修改为新的给定值。
原理
CAS操作的组成,CAS操作通常涉及三个主要参数:
- 内存位置(V):这是要更新的变量的内存地址。
- 预期原值(A):这是线程期望在执行CAS操作时内存位置V的值。
- 新值(B):如果内存位置V的值与预期原值A相等,那么CAS操作将V更新为这个新值B。
CAS操作的步骤:
- 比较:线程检查内存位置V的当前值是否与预期原值A相等。
- 交换:如果当前值与预期原值相等,线程将内存位置V的值更新为新值B。
- 返回:CAS操作返回内存位置V的原始值(在更新之前)。
CAS操作的特点:
- 原子性:CAS操作是原子的,这意味着它作为一个不可分割的操作执行,没有其他线程可以在CAS操作执行的过程中插入其他操作。
- 非阻塞:与使用锁相比,CAS操作不会使线程进入阻塞状态。如果CAS操作失败(即当前值不等于预期原值),线程可以选择重试。
- 自旋:在CAS操作失败时,线程可能会进入自旋等待状态,即不断重试CAS操作,直到成功为止。这可能导致CPU资源的浪费,特别是在高竞争环境下。
CAS操作的问题:
- ABA问题:如果一个值原来是A,变成了B,又变回A,那么CAS操作会认为值没有变化,因为它比较的是A。这可能会导致一些逻辑错误。为了解决这个问题,通常使用版本号或时间戳来区分不同的值。
- 伪共享:在多核处理器上,如果多个线程频繁访问同一缓存行,可能会导致性能下降。这是因为缓存行的一致性需要在多个核心之间同步,即使它们访问的是不同的数据。
- 循环时间长开销:在高竞争环境下,CAS操作可能会失败很多次,导致线程在自旋等待中消耗大量CPU资源。
API
在c++11中提供了原子操作库,也就是<atomic>。提供了两个API:
- std::atomic<T>::compare_exchange_weak(T &expected, T desired)
- std::atomic<T>::compare_exchange_strong(T &expected, T desired)
函数内部实现的就是比较与交换,expected是期待值,也就是作比较的值,desired值是目标值,也就是修改值。weak与strong两个版本实现原理是一样的,只是如字面意思,weak较弱。
weak不保证atomic<T>值与expected值相等的时候也会做交换,可能会在没有实际发生内存值变化的情况下返回false
。这意味着即使内存位置的值在比较之后和预期值相等,也可能因为其他线程的干预而失败。
strong在CAS操作失败时会提供更强的保证。它会在比较失败后立即重试,直到成功或直到内存位置的值不再等于预期值。它可能会在高竞争环境下导致更多的CPU资源消耗。
#include<iostream>
#include<atomic>
std::atomic<int> cnt(3000);
int cmp=3000;
int des=1000;
bool exchanged=false;
int main(){
exchanged=cnt.compare_exchange_strong(cmp,des);
std::cout<<exchanged<<std::endl;
exchanged=cnt.compare_exchange_weak(cmp,des);
std::cout<<exchanged<<std::endl;
return 0;
}
我们尝试对原子变量cnt进行CAS操作,得出结果:第一次成功,第二次失败。
无锁队列
我们常见的,普通的生产者-消费者模型队列,在多线程环境下加锁与解锁是需要时间的,线程的上下文切换代价也比较大,此外还有cache损坏等等,种种原因会导致性能下降,这时候无锁队列会是更明智的选择。无锁队列更适合非耗时任务,因为频繁地锁操作在整个系统占比一旦过大就会降低整体性能。
原理与实现
我们知道,原子操作和CAS是不需要锁的,那么可以使用原子操作+CAS+链表来实现无锁队列。无锁队列就拨云见日、茅塞顿开了。此外我们也可以选择使用数组,这里我们使用链表。
首先来看原子操作,如果我们将一个变量定义为原子变量,那么我们对这个变量的所有操作都将会是原子操作,如果不明白可以看我的另一篇博客:原子操作与锁。我们可以将链表节点设置为原子变量:
public:
struct Node {
T data;//数据
std::atomic<Node*> next;
Node(T const& data) : data(data), next(nullptr) {}
};
private:
std::atomic<Node*> head;//头节点
std::atomic<Node*> tail;//尾节点
再来看CAS操作,我们可以利用CAS来实现入队与出队操作。入队时,我们先用load读取当前尾部的节点值,再用获取的尾部值与当前尾部值比较,如果相同,就说明这个值没有被其他线程修改(我们使用weak,这样就不会替换相同值),如果不同,就说明在这期间其他线程已经修改了这个值,我们就要循环到下一次重新获取尾部值,这就是CAS的使用场景;出队时,我们同样使用load获取头部值,只不过需要判断,如果头尾指针相同说明队列为空。这时候出队失败。
void enqueue(T const& data) {
Node* new_node = new Node(data);
Node* old_tail = tail.load();
while (true) {
old_tail->next = new_node;
if (tail.compare_exchange_weak(old_tail, new_node)) {
return;
}
}
}
bool dequeue(T& result) {
Node* old_head = head.load(std::memory_order_relaxed);//无顺序
while (true) {
if (old_head == tail.load(std::memory_order_relaxed)) {
return false; // 队列为空
}
Node* next = old_head->next.load(std::memory_order_relaxed);
if (head.compare_exchange_weak(old_head, next)) {
result = std::move(next->data);
delete old_head;
return true;
}
}
}
咱们将这两个操作封装成一个简单的无锁队列,测试一下。
#include<iostream>
#include<atomic>
#include<thread>
#include<vector>
#include<cassert>
template <typename T>
class unlockQueue{
public:
struct Node{
T data;
std::atomic<Node*>next;
Node(T const& data):data(data),next(nullptr){}
};
private:
std::atomic<Node*>head;
std::atomic<Node*>tail;
public:
unlockQueue():head(new Node(T())),tail(head.load(std::memory_order_relaxed)){}
~unlockQueue(){
while(Node*const old_head=head.load(std::memory_order_relaxed)){
head.store(old_head->next,std::memory_order_relaxed);//存储下一个节点值
delete old_head;//删除旧节点值
}
}
unlockQueue(const unlockQueue&other)=delete;//禁用拷贝构造
unlockQueue&operator=(const unlockQueue&other)=delete;//禁用赋值操作
void enqueue(T const& data) {
Node* new_node = new Node(data);
Node* old_tail = tail.load();
while (true) {
old_tail->next = new_node;
if (tail.compare_exchange_weak(old_tail, new_node)) {
return;
}
}
}
bool dequeue(T& result) {
Node* old_head = head.load(std::memory_order_relaxed);
while (true) {
if (old_head == tail.load(std::memory_order_relaxed)) {
return false; // 队列为空
}
Node* next = old_head->next.load(std::memory_order_relaxed);
if (head.compare_exchange_weak(old_head, next)) {
result = std::move(next->data);
delete old_head;
return true;
}
}
}
};
void producer(unlockQueue<int>& queue, int start, int count) {//生产,往队列里加任务
for (int i = 0; i < count; ++i) {
queue.enqueue(start + i);
}
}
void consumer(unlockQueue<int>& queue) {//消费,从队列取任务
int value;
while (true) {
if (!queue.dequeue(value)) {
break;
}
std::cout << "Consumed: " << value << std::endl;
}
}
int main() {
const int producer_count = 2;
const int consumer_count = 2;
const int items_per_producer = 50;
unlockQueue<int> queue;
std::vector<std::thread> producers;
std::vector<std::thread> consumers;
// Start producers
for (int i = 0; i < producer_count; ++i) {
//每个生产者线程开始入队整数的起始值。由于有多个生产者线程,每个线程都会从不同的起始值开始入队,以避免它们尝试入队相同的值。
producers.emplace_back(producer, std::ref(queue), i * items_per_producer, items_per_producer);
}
// Start consumers
for (int i = 0; i < consumer_count; ++i) {
consumers.emplace_back(consumer, std::ref(queue));
}
// Join producers
for (auto& producer : producers) {
producer.join();
}
// Join consumers
for (auto& consumer : consumers) {
consumer.join();
}
std::cout << "All items have been consumed." << std::endl;
return 0;
}
在实际项目中还需按情况修改与调整。