关于锁和同步(一)原子操作和非原子操作

在多线程编程中,经常利用到锁来实现同步,所以准备写一个系列的博客来将自己对锁的理解给大家来分享一下。

1. 为什么需要锁

使用锁的目的是将非原子操作封装成一个原子操作,不被其他相关的线程打断。

说起来都拗口,还是来看一个实例!

有下面的代码:

long count = 0;

 

void func()

{

  count++;

}

如果同时启动10个线程执行func函数,所有线程都执行完成后count的值是多少?

扫描二维码关注公众号,回复: 1941080 查看本文章

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;

道理和上面的 整型一样,就不多说了。。。

在涉及到多线程同步,涉及到锁的时候一定要小心。。。 

后面还会持续更新,关于锁更多的内容,欢迎大家持续关注。。。。。

猜你喜欢

转载自blog.csdn.net/zhangqhn/article/details/80876177