无锁编程(四) - CAS与ABA问题

CAS

一般采用原子级的read-modify-write原语来实现Lock-Free算法,其中LLSCLock-Free理论研究领域的理想原语,但实现这些原语需要CPU指令的支持,非常遗憾的是目前没有任何CPU直接实现了SC原语。根据此理论,业界在原子操作的基础上提出了著名的CASCompare-And-Swap)操作来实现Lock-Free算法,Intel实现了一条类似该操作的指令:cmpxchg8

CAS原语负责将某处内存地址的值(1个字节)与一个期望值进行比较,如果相等,则将该内存地址处的值替换为新值,CAS 操作伪码描述如下:

Bool CAS(T* addr, T expected, T newValue)

{

         if(*addr == expected )

         {

                   *addr=  newValue;

                   returntrue;

         }

         else

                   returnfalse;

}

CAS实际操作

do

{

         备份旧数据;

         基于旧数据构造新数据;

}while(!CAS(内存地址,备份的旧数据,新数据))

 

就是指当两者进行比较时,如果相等,则证明共享数据没有被修改,替换成新值,然后继续往下运行;如果不相等,说明共享数据已经被修改,放弃已经所做的操作,然后重新执行刚才的操作。容易看出CAS操作是基于共享数据不会被修改的假设,采用了类似于数据库的commit-retry的模式。当同步冲突出现的机会很少时,这种假设能带来较大的性能提升。

CASLinux解法

cmpxchg先比较内存地址的值是否与传入的值相等,如果相等则执行xchg逻辑。

inline int CAS(unsigned long* mem, unsignedlong newval, unsigned long oldval)

{

         __typeof(*mem) ret;

         //这里测试的使用64位系统,如果是32位,这里使用cmpschgl

         __asm__volatile ("lock; cmpxchgq %2,%1"

                                                        :"=a"(ret), "=m"(*mem)

                                                        :"r"(newval), "m"(*mem), "0"(oldval));

         returnret==oldval;

}

CAS举例(简单应用AtomicInc

#include <stdio.h>
#include <pthread.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/time.h>
#include <stdint.h>

int count = 0;

inline int CAS(unsigned long* mem, unsigned long oldval, unsigned long newval)
{
	__typeof (*mem) ret;
	// 这里测试的使用64位系统,如果是32位,这里使用cmpschgl
	__asm __volatile ("lock; cmpxchgq %2,%1"
						: "=a"(ret), "=m"(*mem)
						: "r"(newval), "m"(*mem), "0"(oldval));
	return ret==oldval;
}

void AtomicInc(int* addr)
{
	int oldval;
	int newval;
	do
	{
		oldval = *addr;
		newval = oldval+1;
	} while(!CAS((unsigned long*)addr, oldval, newval));
}

void *test_func(void *arg)
{
	int i=0;
	int confict = 0;
	for(i=0;i<2000000;++i)
	{
		AtomicInc(&count);
	}
	return NULL;
}

int main(int argc, const char *argv[])
{
	pthread_t id[20];
	int i = 0;

	uint64_t usetime;
	struct timeval start;
	struct timeval end;
	
	gettimeofday(&start,NULL);
	
	for(i=0;i<20;++i)
	{
		pthread_create(&id[i],NULL,test_func,NULL);
	}

	for(i=0;i<20;++i)
	{
		pthread_join(id[i],NULL);
	}
	
	gettimeofday(&end,NULL);

	usetime = (end.tv_sec-start.tv_sec)*1000000+(end.tv_usec-start.tv_usec);
	printf("count = %d, usetime = %lu usecs\n", count, usetime);
	return 0;
}

CAS举例(复杂应用)

struct Node
{
	Node* next;
	int data;
}
Node* head = NULL;

void push(int t)
{
	Node* node = new Node(t);
	do
	{
		node->next = head;
	} while (!CAS(&head, node->next, node));
}

bool pop(int&t )
{
	Node* current = head;
	while(current)
	{
		if (CAS(&head, current, current->next)) // ABA问题
		{
			t = current->data;
			return true;
		}
		current = head;
	}
	return false;
}

 

ABA问题

一般的CAS在决定是否要修改某个变量时,会判断一下当前值跟旧值是否相等。如果相等,则认为变量未被其他线程修改,可以改。 
但是,相等并不真的意味着未被修改。另一个线程可能会把变量的值从A改成B,又从B改回成A。这就是ABA问题。
很多情况下,ABA问题不会影响你的业务逻辑因此可以忽略。但有时不能忽略,这时要解决这个问题,一般的做法是给变量关联一个只能递增、不能递减的版本号。在compare时不但compare变量值,还要再compare一下版本号。 
Java
里的AtomicStampedReference类就是干这个的。


 

<think>好的,用户想了解C++中处理无编程ABA问题的解决方案和示例代码。首先,我需要回忆一下ABA问题的定义以及常见的解决方法。ABA问题通常发生在使用CAS(比较并交换)操作时,当一个变量的值被多次修改后回到原值,导致CAS误判没有变化,从而引发数据不一致的问题。 接下来,我需要查阅用户提供的引用内容。引用[1]提到ABA问题在无算法中的重要性,并指出解决方法是使用版本号或标签指针。引用[2]具体提到了C++11中的无队列,通过版本号解决ABA问题,并提到Java的AtomicStampedReference类。引用[3]则列举了多种解决方案,包括标签指针、危险指针、RCU和内存回收器,但用户可能需要C++的具体示例。 用户需要的是C++的示例代码,所以重点应该放在版本号或标签指针的实现上。在C++中,可以使用std::atomic库中的compare_exchange_weak或compare_exchange_strong来实现CAS操作。为了加入版本号,可能需要将指针和版本号打包成一个结构体,并使用双字CAS操作,这在某些平台上可能需要特定支持,比如C++的std::atomic<std::uintptr_t>结合位操作。 另外,引用[2]提到了Java的AtomicStampedReference,C++中虽然没有直接对应的类,但可以通过结构体和原子操作模拟类似功能。例如,将指针和版本号组合成一个64位的值(假设指针是32位),或者使用双字原子操作(如C++的std::atomic<T*>和单独的版本号原子变量,但需要注意原子性)。 需要注意,不同平台对双字CAS的支持可能不同。例如,x86-64支持128位的CAS操作,可以通过编译器内置函数或特定库实现。不过,为了代码的可移植性,可能需要使用C++标准库中的特性,或者依赖平台特定的扩展。 示例代码部分,可以展示一个使用版本号的简单无栈实现。比如,节点结构包含数据和版本号,push和pop操作中使用CAS并更新版本号。但需要注意,实际中可能需要将指针和版本号打包成一个单独的原子变量,比如使用位操作将版本号存储在指针的高位,或者使用联合体(union)来组合它们。 此外,引用[3]提到的危险指针或RCU可能更适用于复杂情况,但用户可能更倾向于直接的版本号方案。因此,示例代码应侧重于版本号方法,结合C++的原子操作,并展示如何避免ABA问题。 最后,确保代码语法正确,比如正确使用原子操作和内存顺序参数,以及结构体的对齐和打包,防止数据竞争和确保原子性。同时,解释代码的关键部分,帮助用户理解如何通过版本号防止ABA问题。</think>### C++无编程ABA问题的解决方案及示例代码 ABA问题是无数据结构设计中的经典问题,尤其在基于CAS(Compare-and-Swap)的算法中。以下是C++中的解决方案和代码示例: --- #### 解决方案 1. **版本号/标签指针(Tagged Pointer)** 通过将指针版本号组合为一个复合值,每次修改指针时递增版本号。CAS操作需同时验证指针和版本号是否匹配。 - **原理**:即使指针地址相同,若版本号不匹配,CAS仍会失败。 - **C++实现**:利用`std::atomic`对复合结构(指针+版本号)进行原子操作。 2. **内存回收延迟(Hazard Pointer/RCU)** 通过延迟内存释放避免指针被复用,从而消除ABA风险[^3]。 - **适用场景**:复杂数据结构,如无链表、队列。 --- #### 示例代码(版本号方案) 以下是一个简化的无栈实现,使用版本号解决ABA问题: ```cpp #include <atomic> #include <cstdint> // 复合结构:指针 + 版本号 struct Node { int data; Node* next; }; struct TaggedPointer { Node* ptr; uintptr_t version; }; class LockFreeStack { private: std::atomic<TaggedPointer> top; public: void push(Node* new_node) { TaggedPointer old_top = top.load(std::memory_order_relaxed); TaggedPointer new_top; do { new_node->next = old_top.ptr; new_top.ptr = new_node; new_top.version = old_top.version + 1; // 版本号递增 } while (!top.compare_exchange_weak( old_top, new_top, std::memory_order_release, std::memory_order_relaxed )); } Node* pop() { TaggedPointer old_top = top.load(std::memory_order_relaxed); TaggedPointer new_top; do { if (old_top.ptr == nullptr) return nullptr; new_top.ptr = old_top.ptr->next; new_top.version = old_top.version + 1; // 版本号递增 } while (!top.compare_exchange_weak( old_top, new_top, std::memory_order_acquire, std::memory_order_relaxed )); return old_top.ptr; } }; ``` **关键点**: - `TaggedPointer`将指针版本号绑定,确保每次修改后版本号唯一。 - `compare_exchange_weak`同时检查指针和版本号,若其他线程已修改栈顶,则CAS失败重试。 - 内存顺序参数(如`std::memory_order_acquire`)需根据实际场景调整[^2]。 --- #### 其他方案对比 | 方案 | 优点 | 缺点 | |-----------------|--------------------------|--------------------------| | 版本号 | 实现简单,通用性强 | 需原子操作支持复合结构 | | Hazard Pointer | 内存安全,无需复合操作 | 实现复杂,性能开销较高 | | RCU | 高并发读性能 | 写操作延迟高,适用场景窄 | --- 相关问题
评论 1
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值