原子操作CAS与锁实现
文章目录
在多线程并发场景下,不同线程间的指令执行先后顺序是不确定的。对于临界资源的访问,需要互斥进行。
例:多线程并发,执行自增操作,若非互斥访问临界资源,则自增操作会存在覆盖问题
实现互斥访问临界资源的方式是锁机制和原子操作。
1、linux 的锁机制
当一个线程访问的时候,需要加上锁以防止另外的线程对它进行访问,实现资源的独占。在一个时刻只能有一个线程掌握某个互斥锁,拥有上锁状态的线程能够对共享资源进行操作。
1.1、互斥锁 mutex
特点:只要一个线程获取了锁,其他线程则不能获取,竞争失败的线程让出 cpu,陷入休眠。。
pthread_mutex_t mutex;
pthread_mutex_init(&mutex, NULL);
pthread_mutex_lock(&mutex)
pthread_mutex_unlock(&mutex)
pthread_mutex_destroy(&mutex)
2.2、自旋锁 spinlock
特点:轮询忙等待,获取不到锁就一直等待。
自旋锁采用原地等待的方式解决资源冲突。也就是说,一个被争用的自旋锁使得请求它的线程在等待锁重新可用的期间进行自旋(cpu 空转),反复检测锁有没有被解开。
在单核 CPU 上,自旋锁需要抢占式的调度器,否则无法使用,自旋线程永远不会放弃 CPU。
pthread_spin_init(pthread_spinlock_t *, int pshared);
// pshared: PTHREAD_PROCESS_SHARED:多进程共享,PTHREAD_PROCESS_PRIVATE:本进程使用
pthread_spin_lock(pthread_spinlock_t *);
pthread_spin_trylock(pthread_spinlock_t *);
pthread_spin_unlock(pthread_spinlock_t *);
int pthread_spin_destroy(pthread_spinlock_t *);
自旋锁与互斥锁的区别
- 互斥锁加锁失败后,线程挂起,释放 CPU 给其他线程。缺点线程切换带来系统开销。
- 自旋锁适加锁失败后,线程忙等,一直占用 CPU 直到它拿到锁。缺点 cpu 空转浪费资源,适用于在短期间内进行轻量级的锁定。
1.3、读写锁 rwlock
读写锁适用于单写多读的情况。将操作分为读、写两种方式,读模式锁定时多个线程共享,写模式锁住时则单线程独占,所以又称作共享-独占锁。
- 写独占:写锁占用时,其他线程加读锁或者写锁时都会阻塞
- 读共享:读锁占用时,其他线程加写锁时会阻塞,加读锁会成功
读写锁的策略
- 强读同步:读锁优先,只要写锁没有占用那么就可以加读锁
- 强写同步:写锁优先,只能等到所有正在等待或者执行的写锁执行完成后才能加读锁
大部分读写锁的实现都采用的是强写同步策略,这样做的目的主要是为了避免写饥饿,在多读少写的情况下防止数据修改延迟过高,参考读者写者问题的写优先。
pthread_rwlock_t rwlock;
pthread_rwlock_init(&rwlock, NULL);
pthread_rwlock_rdlock(&rwlock);
/*------ 临界资源读操作 ------*/
pthread_rwlock_unlock(&rwlock);
pthread_rwlock_wrlock(&rwlock);
/*------ 临界资源读操作 ------*/
pthread_rwlock_unlock(&rwlock);
pthread_rwlock_destroy(&rwlock);
2、原子操作
2.1、嵌入汇编语法
C语言使用__asm__
声明一个内联汇编表达式,volatile
向 gcc 声明不允许对该内联汇编优化。
C语言嵌入汇编语法格式如下:
__asm__ volatile(Instruction List : Output : Input : Clobber/Modify);
Instruction List
:汇编指令序列,指令间使用分号;
或换行符\n
分开。指令中的操作数可以使用占位符,操作数占位符最多10个,:%0, %1, …, %9
Output
:输出Input
:输入Clobber/Modify
:通知 gcc 当前嵌入汇编语句可能会对哪些寄存器或内存进行修改
用嵌入汇编实现自增的原子操作
__asm__ volatile (
"lock; xaddl %2, %1;" // 指令1:lock; 指令2: xaddl, 操作数占位符:%1, %2
: "=a" (old) // 输出:结果放入通用寄存器eax
: "m" (*value), "a" (add) // 输入:操作数1(内存),操作数2(寄存器eax)
: "cc", "memory" // 编译方式,内存
);
2.2、无锁 CAS
Compare And Swap,比较后交换。CAS 是一种无锁的解决方案,也是一种基于乐观锁的操作,解决多了线程并行情况下使用锁造成性能损耗。
CAS 原子操作包含三个操作数——内存位置(V)、预期原值(A)和新值(B)。如果内存位置的值与预期原值一致,则将该值更新为新值。否则,不做任何操作。
// CAS 操作
if (V == A) {
V == B;
}
CAS 操作在操作系统底层 (x86) 中对应的是cmpxchg
汇编指令
// 简化后的内联汇编 __cmpxchg 函数接口
static inline unsigned long __cmpxchg(volatile void *ptr, unsigned long old,
unsigned long new)
{
unsigned long prev;
__asm__ __volatile__(LOCK_PREFIX "cmpxchgl %1,%2"
: "=a"(prev)
: "r"(new), "m"(*__xg(ptr)), "0"(old)
: "memory");
return prev;
return old;
}
gcc 提供了两个函数接口支持 CAS 原子操作
bool __sync_bool_compare_and_swap (type *ptr, type oldval, type newval, ...);
type __sync_val_compare_and_swap (type *ptr, type oldval, type newval, ...);
我们也可以使用这些函数接口,用 CAS 原子操作实现自增原子操作。
int tmp = *pcount;
// cas 操作
if (__sync_bool_compare_and_swap(pcount, tmp, tmp + 1)) {
// 成功更新 pcount 的值
++i;
}