如何帮罗小猪的时间管理进一步提高性能?无锁并发

之前的文章里,用罗小猪无缝切换多个女友的故事类比过多线程,并且说为了防止被女友发现可以采用互斥锁的机制来保证线程安全。


但其实除了上锁之外,还有另外一种更高性能的方式也可以保证线程安全,叫做无锁并发


如何帮罗小猪的时间管理进一步提高性能?无锁并发


谈无锁并发之前,我预设大家都是对多线程有基础的了解的,如果完全不了解的可以先移步这一篇文章《从罗小猪时间管理的角度剖析Linux线程锁》读一读罗小猪的风流韵事,品一品时间管理大师的秘术。


于是我们知道,并发是大型软件不可避免会涉及到的一个问题。你可能要一边运行某个任务,一边还要监听别的任务的指令;或者作为服务端,可能需要为每个连接成功的客户端给予及时的响应,以保证他的用户体验。


在C/C++里,大部分并发需求都会用多线程的方式来实现。


多线程和多进程最主要的区别就是,多进程会为每个进程都分配各自独立的内存空间,而多线程会共享主线程的内存空间。因此进程间通信有信号、管道、共享内存、socket、消息队列等多种多样的手段,而线程间通信就用全局变量或类内成员变量就好了,当然这本质上其实也是共享内存。


既然说到共享,那就一定会出现竞争。当多个线程同时对一块内存进行读写时,势必会出现张冠李戴、错综混乱的情况,因此锁机制应运而生。

嵌入式物联网需要学的东西真的非常多,千万不要学错了路线和内容,导致工资要不上去!

无偿分享大家一个资料包,差不多150多G。里面学习内容、面经、项目都比较新也比较全!某鱼上买估计至少要好几十。

点击这里找小助理0元领取:嵌入式物联网学习资料(头条)


如何帮罗小猪的时间管理进一步提高性能?无锁并发


如何帮罗小猪的时间管理进一步提高性能?无锁并发


我们当然可以用互斥量来为某个代码段加锁,来保证同一时间只有一个线程在操作这块共享区域。不过加锁、竞争锁的开销很大,而且稍有不慎可能会造成死锁,导致程序无法继续运行。


那么是否有一种不加锁的方式来保证并发时共享内存的安全呢?


无锁并发的答案就是原子量atomic。


std::atomic<int> variable;


一旦我们用atomic来修饰某个变量,那么这个变量的操作就不可被打断。即使有很多个线程都可以访问操作这个变量,但由于这个变量的原子性,不可中断的特性,就不会出现某个线程刚操作了一半,却被另一个线程篡改的情况,因此也就实现了并发的安全保证。并且无锁并发的效率,理论上要高于有锁。


不过并不是所有的原子量都是无锁的,可能有的编译器版本底层还是用了互斥量加锁解锁来实现的原子量,所以为了避免折腾半天又跳回了同一条贼船,我们可以通过is_lock_free()函数来查看原子量是否是无锁实现。


如何帮罗小猪的时间管理进一步提高性能?无锁并发


看到这儿的读者是否会有一丝疑惑,既然无锁并发的关键---原子量这么牛叉,为什么还会有有锁并发的存在?


原子量和锁的区别是,基本上原子量能做的,通过加锁也都能做到。但反之则不行,原子量只能保证自己是不被中断的,但无法对代码段的执行提供保护。


这么一比,好像原子量有点弱,难道超过2行的代码,有非原子量的代码,就必须得换成锁了吗?


好在原子量给自己找了个帮手:内存序


假设这样一种场景:


flag是个普通的全局变量,可被多个线程访问。


线程A里做计算,计算完成后将flag置位;


// 计算值
data = calculate();
//设置flag,通知其他任务值已可用
flag =
true;


同时线程B监听flag的状态,一旦发现flag被置位,则开始对data做另外的处理。


if(flag){
doSomething(data);
}


而编译器这个大聪明,很有可能会自以为是的将flag=true这条语句放到calculate计算之前


因为它觉得两者似乎是不同的内存、不同的寄存器。它发现calculate在里面计算还得一段时间,它就先把下面的指令也执行了,反正是两个不同的地址。并且它还将这个操作自信的称之为编译器优化


这样会带来的后果就是,线程B可能会拿到未经过calculate计算的data而进行处理,完全违背了我们设计这段程序的初衷。


用加锁的方式的话,我们可以将这段代码用互斥锁保护起来,保证data计算完成后,flag才会被其他线程拿到;


//线程A
{
mylock.lock();
// 计算值
data = calculate();
//设置flag,通知其他任务值已可用
flag = true;
mylock.unlock();
}
//线程B
{
mylock.lock();
if(flag){
doSomething(data);
}
mylock.unlock();
}


而如果用原子量+内存序的方式的话,则线程A可以这么实现:


//计算值
data = calculate();
//设置flag,通知其他任务值已可用
flag.store(memory_order_release);


如此便可以保证上述代码的正常执行次序,防止被其他线程在flag未设置之前拿到计算值。


即C++原子量提供了load和store两个函数,表示读取和写入。另外提供了内存序,release在写入时使用,表示释放;acquire在读取时使用,表示获取


store(memory_order_release)会对影响所及的内存区执行一个所谓的release操作,确保此前所有内存操作(all prior memory operations)不论是否为atomic,在store发挥效用之前都变成“可被其他线程看见”。


load(memory_order_acquire)会对影响所及的内存区执行一个所谓的acquire操作,确保随后所有内存操作(all following memory operations)不论是否为atomic,在load之后都变成“可被其他线程看见”。


这里是翻译的C++标准库的原话,这个“可被其他线程看见”的意思就是,其他线程在拿到这个变量时,它的值就是最新的值,或者说对它的操作都已经生效了。


再说的直白一点就是,由于原子量内存序的存在,编译器这个大聪明,不会把flag的置位放到calculate之前了,它会确保store之前的动作,都在store之前发生。而在load之后的动作,都在load之后发生。所以release和acquire经常成对出现,来保证无锁并发时线程之间的同步


如何帮罗小猪的时间管理进一步提高性能?无锁并发


不知道各位看到这里,有没有想起来volatile这个关键字。


volatile给我们印象是,它是用来防止编译器优化的;是告诉编译器它修饰的变量是易变的,所以每次读取时都从变量内存地址处读取;是应该去修饰那些多任务环境下各任务间共享的标志位的。


乍一看,好像跟上述原子量+内存序有那么一点像,那么它们之间到底有没有区别呢?我得说一声,区别老大了,而且应用的场景可以说是完全不同。


多数教材对volatile的作用解释也有些过时,现代C++里其实不该再用volatile来保证多任务的安全。


篇幅所限,我这里先埋个梗,下一篇文章详细聊聊volatile这个面试高频关键字,和现代C++里的原子量+内存序的区别和联系,彻底搞定这个知识点。


原文链接:https://mp.weixin.qq.com/s/ffnFSgVcipXfCg0mvIG5-Q

转载自:李纳克斯Linux

文章来源于李纳克斯

本文来源网络,免费传达知识,版权归原作者所有。如涉及作品版权问题,请联系我进行删除。

发表评论
留言与评论(共有 0 条评论) “”
   
验证码:

相关文章

推荐文章