多线程安全CAS实现的无锁

时间:2021-03-19 12:12:03

实现无锁队列的关键点有二:

1、各个平台的原子操作或者说CAS原语;

2、ABA问题的理解和解决。

首先来说说这个CAS原语,所谓CAS(Compare And Swap)即比较并交换,在 Intel 处理器中,比较并交换通过指令的 cmpxchg 系列实现。CAS有三个操作数: 内存位置(V)、预期原值(A)和新值(B)。如果内存位置V的值与预期A原值相匹配,那么处理器会自动将该位置值更新为新值B。否则,处理器不做任何操作。无论哪种情况,它都会在 CAS 指令之前返回该位置的值。(在 CAS 的一些特殊情况下将仅返回 CAS 是否成功,而不提取当前值。)CAS 有效地说明了“我认为位置 V 应该包含值 A;如果包含该值,则将 B 放到这个位置;否则,不要更改该位置,只告诉我这个位置现在的值即可。”CAS的C语言实现如下:

inline bool CAS2(pointer_t *addr, pointer_t &old_value, pointer_t &new_value)
{
bool ret;
__asm__ __volatile__(
"lock cmpxchg16b %1;\n"
"sete %0;\n"
:"=m"(ret),"+m" (*(volatile pointer_t *) (addr))
:"a" (old_value.ptr), "d" (old_value.tag), "b" (new_value.ptr), "c" (new_value.tag));
return ret;
}

这其中包括了内联汇编代码(这个可以去看看AT&T汇编语法),其中能够支持多线程的并行安全执行的秘诀就在 "lock cmpxchg16b %1;\n"这句中的“lock”,这个lock就和我们基本多线程编程中的锁相当,只不过,这个lock不再是普通的锁,它锁的是地址总线,在多核编程情景下,当已经有CPU的线程A已经访问某地址时,这时地址总线会被锁定,其他CPU核心的线程B无法再访问当前地址,直到A访问完毕之后,其他线程如B才可能访问该地址,这样就保证了线程A访问该地址的原子性操作。

再来讲讲这个ABA问题。在进行CAS操作时,因为在更改V值之前,CAS主要是通过访问V的值是否仍然和A相等,所以,在第一次读取V以及对V执行CAS操作之前,如果有其他线程将V的值先从A改为B,而另外的线程又将V的值从B改回了A,这样会使CAS算法混乱。显然,在这种情况下,CAS的操作会成功,这类的问题称为ABA问题。要解决这类问题,就是不再重用A,通常的做法是用标记或版本编号与进行CAS操作的每个值相关联,并原子的更新值和标记。


CAS实现的无锁

#include <stdio.h>

#include <stdlib.h>

#include <pthread.h>

#include <unistd.h>

#include <time.h>

#include "timer.h"



int mutex = 0;

int lock = 0;

int unlock = 1;



static volatile int count = 0;

void *test_func(void *arg)

{

int i = 0;

for(i = 0; i < 2000000; i++)

{

while (!(__sync_bool_compare_and_swap (&mutex,lock, 1) ))usleep(100000);

count++;

__sync_bool_compare_and_swap (&mutex, unlock, 0);

}

return NULL;

}



int main(int argc, const char *argv[])

{

Timer timer;

timer.Start();

pthread_t thread_ids[10];

int i = 0;



for(i = 0; i < sizeof(thread_ids)/sizeof(pthread_t); i++)

{

pthread_create(&thread_ids[i], NULL, test_func, NULL);

}



for(i = 0; i < sizeof(thread_ids)/sizeof(pthread_t); i++)

{

pthread_join(thread_ids[i], NULL);

}



timer.Stop();

timer.Cost_time();

printf("结果:count = %d\n",count);



return 0;

}