一、多线程临界资源竞争问题
首先,锁是用来解决多线程临界资源竞争的问题的。在对各锁进行对比之前,需要先了解多线程的资源为什么会被竞争。
比如对于语句
(&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看不懂的地方进行解析:
__asm__ volatile:
C嵌套汇编语言的语法格式lock:
表示这后面紧接的这条语句是原子操作,不可分割,不可中断xaddl %2, %1:
汇编xaddl指令(自行查找),%2为此后声明的第3个变量,%1为第二个,0%为第一个"=a"(ret):
a表示寄存器eax,ret表示外界变量ret,=为约束作用,表示这是一个输出参数,默认是输入参数。表示最后将寄存器eax的值存储在ret中(%0)"m"(value):
m表示通用寄存器。表示将value的值存储在一个通用寄存器中(%1)"a"(add):
a表示寄存器eax。表示将add的值存储在eax寄存器中(%2)"cc":
提示修改了程序状态寄存器(如标志位)。"memory":
提示可能会修改内存数据。
4. 比较
- 互斥锁的原理为操作系统层级的线程状态切换,因此等待的时间为线程状态切换耗费的时间。
- 自旋锁的原理为原地等待其他线程操作完成,因此等待的时间为任务耗费的时间。
- 原子操作纯纯减少命令节约时间.
因此,使用场景如下:
- 原子操作:只要你能做到,最快
- 互斥锁:锁的内容较多(等待时间可能较长,大于更改线程状态的时间)
- 自旋锁:锁的内容很少(更改线程状态的代价 > 等待解锁的代价)
三、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++知识,感兴趣的可以关注一手,共同讨论学习进步。