在多线程编程中,经常利用到锁来实现同步,所以准备写一个系列的博客来将自己对锁的理解给大家来分享一下。
1. 为什么需要锁
使用锁的目的是将非原子操作封装成一个原子操作,不被其他相关的线程打断。
说起来都拗口,还是来看一个实例!
有下面的代码:
long count = 0;
void func() { count++; } |
如果同时启动10个线程执行func函数,所有线程都执行完成后count的值是多少?
10,对吗?正确答案是 不确定,从2-10都有可能。
你没看错,从2-10都有可能,导致这个问题的原因是 count++ 不是一个原子操作,编译成汇编,如下所示:
MOV eax, [count] INC eax MOV [count], eax |
在cpu执行时
第一步,先将 count所在内存的值加载到寄存器;
第二步,将寄存器的值自增1;
第三步,将寄存器中的值写回内存。
所以当第一个线程将count值加载到寄存器,并完成自增1,这时寄存器中的值为2,如果此时cpu调度将此线程中断,并执行完其它线程后,再将此线程调度执行,此时,会将2写入到count。count最后的值就成了2!!!
3-10的结果类似... ...
所以要想保证上面实例的结果正确只需要保证count++ 在执行中不被打断即可。
方法一 使用锁,代码如下所示:
#include<mutex>
std::mutex cntMutex; long count = 0;
void func() { std::unique_lock<std::mutex> cntLock(cntMutex); count++; } |
方法二 使用原子函数:
如果只是简单的改变一个变量的值,在windows上提供Interlocked系列函数,能提供比互斥锁高很多的性能,具体的介绍可以去查msdn,修改后的代码如下所示:
#include<Windows.h> volatile long count = 0;
void func() { InterlockedExchangeAdd(&count, 1); } |
大家是不是已经理解了之前的描述:使用锁的目的是将非原子操作封装成一个原子操作,不被其他相关的线程打断。
其实更准确的说时,当一个线程获得锁之后,其它线程要获得相同的锁需要等待锁被释放。
好了,有些人不要钻牛角尖,这里说的是互斥锁,当然读写锁中多个读是可以一起执行的,以后有机会再说。。。。
2. 哪些操作是原子操作
上面说过,锁的作用是保证执行过程中不会被相关的线程打断。那如果说一个操作是原子操作了,那么在它上面加锁就没有意义了。
- 整型赋值语句
代码 count = 5, 产生的汇编如下:
MOV eax, 5 MOV [count], eax |
注:我记得不能直接将一个立即数存储到内存中,要借助寄存器。希望没记错。。。
这里虽然是两条汇编,但是前一条,不依赖内存中的数据,不用担心中间被中断,内存中数据被修改的问题。所以,这是个原子操作,加不加锁都一样。
但是 count += 5; 及上面的 count++; 不是原子操作。
if (count == 5) 这条语句只是从内存中加载数据到寄存器,所以是个原子操作。
所以:有原子操作和非原子操作混用时,还是要加锁。
所有的操作都是原子操作,没有和非原子操作混用时,可以不用加锁,提高性能。
- bool类型的操作
bool flag = true;
下面的操作都是原子操作:
flag = false;
if (flag)
if (!flag)
下面的操作不是原子操作
flag = !flag;
道理和上面的 整型一样,就不多说了。。。
在涉及到多线程同步,涉及到锁的时候一定要小心。。。
后面还会持续更新,关于锁更多的内容,欢迎大家持续关注。。。。。