C/C++ 多线程锁的比较及CAS实现

一、多线程临界资源竞争问题

首先,锁是用来解决多线程临界资源竞争的问题的。在对各锁进行对比之前,需要先了解多线程的资源为什么会被竞争。
比如对于语句

(&count)++;

其实在CPU执行的过程中将相当于3条汇编语句进行执行:

mov  [count], eax;		// 将内存中变量count所在地址中的值(count的值)传入到寄存器eax中
inc eax;				// 使寄存器eax中的值自增
mov eax, [count];		// 将寄存器eax中的值移动到内存中变量count所在的地址

而我们知道(不知道现在就知道了):操作系统会在CPU执行指令的时候,对线程进行切换,这是多线程临界资源竞争问题的根本原因。那么若是两个线程切换时,执行顺序正常则不会出现问题:

// 线程一
mov  [count], eax;
inc eax;
mov eax, [count];	
// CPU在此处进行切换
// 线程二
mov  [count], eax;
inc eax;
mov eax, [count];	

但若是切换出现在了奇怪的地方(啊,那里不可以!),那么就会出现一些意料以外的现象:

// 假设此时count = 50
// 线程一
mov [count], eax;		// count = 50; 线程一的eax = 50
// 线程二
mov [count], eax;		// count = 50; 线程二的eax = 50
inc eax;				// count = 50; 线程二的eax = 51
mov eax, [count]		// count = 51;线程二的eax = 51
// 线程一
inc eax;				// count = 51; 线程一的eax = eax++ =51
mov eax,[count]			// count = 51; 线程一的eax = 51

这里我们假设读者已知晓,在汇编中进行线程切换时,会将原线程的寄存器状态推进栈,待切回该线程时,从栈中重新取出原线程寄存器状态。
若是正常情况,两个线程分别进行一次count++,则最终count结果应为52,但出现以上状况时便只能得到51的结果,这便是多线程会出现资源竞争的原因。

二、多线程不同锁的对比

为解决多线程的临界资源竞争问题,引入锁的操作:

1.互斥锁

使用方法:

pthread_mutex_lock(&mutex);		// 上锁,mutex为提前声明且初始化好的互斥锁
(*pcount)++;
pthread_mutex_unlock(&mutex);	// 解锁

上锁原理

简单来说,在对mutex进行上锁操作时,会在系统层级上将该线程“挂起”(使用了操作系统底层的同步原语),不再参与线cpu调度(即将处于无法再切换到该线程进行执行的状态),直到解锁时,方可重新变为正常状态,参与调度。
这样就相当于强行在该线程上将这三条汇编语句绑定在一起,若非指令全部完成,则其他的线程无法解锁,也就无法访问count所在的内存。

2.自旋锁

使用方法:

pthread_spin_lock(&spinlock);		// 上锁,spinlock为提前声明且初始化好的互斥锁
(*pcount)++;
pthread_spin_unlock(&spinlock);		// 解锁

上锁原理

所谓自旋,即自己一直在空转,指的是进行“上锁”操作时,不进行线程切换,一直在此处进行while循环轮询访问锁的状态,直到锁的状态变为解锁状态时,方可继续向下操作。
这样也相当于将三条汇编指令绑定在一起,在知晓其他线程正在访问临界资源时,进行自旋等待,直到其他线程完成全部指令后,才轮到自己去访问内存进行操作。

3.原子操作

使用方法

// mov [count],eax; inc eax; mov eax,[count]
//  -> xaddl 1, [count]
// 将三条汇编指令变为一条汇编指令进行执行的函数
int inc(int *value, int add){

	int ret;
	
	__asm__ volatile(
		"lock; xaddl %2, %1;"
		:"=a" (ret)
		:"m"(*value), "a"(add)
		:"cc","memory"
	);

	return ret;
}
// 由此之后,语句count++便完全等价于inc(&count,1)

“上锁”原理

前面提到,多线程出现临界资源抢占问题的原因是count++在汇编中以3条指令进行实现,那么“原子操作”便是通过将3条指令,通过在C中嵌套汇编的方式,自行转为1条指令的函数进行执行,即可解决问题。

语句解析

下面一句一句对xdm看不懂的地方进行解析:

  1. __asm__ volatile: C嵌套汇编语言的语法格式
  2. lock:表示这后面紧接的这条语句是原子操作,不可分割,不可中断
  3. xaddl %2, %1:汇编xaddl指令(自行查找),%2为此后声明的第3个变量,%1为第二个,0%为第一个
  4. "=a"(ret):a表示寄存器eax,ret表示外界变量ret,=为约束作用,表示这是一个输出参数,默认是输入参数。表示最后将寄存器eax的值存储在ret中(%0)
  5. "m"(value):m表示通用寄存器。表示将value的值存储在一个通用寄存器中(%1)
  6. "a"(add):a表示寄存器eax。表示将add的值存储在eax寄存器中(%2)
  7. "cc":提示修改了程序状态寄存器(如标志位)。
  8. "memory":提示可能会修改内存数据。

4. 比较

  1. 互斥锁的原理为操作系统层级的线程状态切换,因此等待的时间为线程状态切换耗费的时间。
  2. 自旋锁的原理为原地等待其他线程操作完成,因此等待的时间为任务耗费的时间。
  3. 原子操作纯纯减少命令节约时间.

因此,使用场景如下:

  1. 原子操作:只要你能做到,最快
  2. 互斥锁:锁的内容较多(等待时间可能较长,大于更改线程状态的时间)
  3. 自旋锁:锁的内容很少(更改线程状态的代价 > 等待解锁的代价)

三、CAS原子操作实现:

CAS:Compare And Swap

逻辑如下:

if(value == expect)
	value = new_value

实现:

int cas(int *value, int expected, int new_value){

	int ret;

	__asm__ volatile(
		"lock; cmpxchgl %3, %1;"
		"sete %0;"
		:"=q"(ret), "+m"(*value)
		:"a"(expected),"r"(new_value)
		:"memory"
	);

	return ret;
}

相信xdm能看的懂了(具体汇编语句的查完应该就能看懂了)。

觉得有帮助可以点一下赞,小编未来也可能会更新一些学习到的C++知识,感兴趣的可以关注一手,共同讨论学习进步。

  • 57
    点赞
  • 25
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值