趣话计算机底层技术一个故事看懂各种锁
Posted 轩辕之风
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了趣话计算机底层技术一个故事看懂各种锁相关的知识,希望对你有一定的参考价值。
我是一个线程,一个卖票程序的线程。
自从我们线程诞生以来,同一个进程地址空间里允许有多个执行流一起执行,效率提升的同时,也引来了很多麻烦。
我们卖票线程的工作很简单,比如票的总数是100,每卖一张就减1,直到变成0售完为止。
以前单线程的时候没啥问题,但多个线程一起执行的时候就发现,有些家伙读取到票数是100,减1后变成99,还没等他把票数写回去,另外有别的线程去读也是100,也做了同样的事,结果卖了两张票,票数才减了1张,一天下来,多卖了很多票,气的人类差点想砸了我们。
原子操作
我们把这问题反馈给了操作系统大哥,他给我们的解决方案是:读取票数->票数减1->写回票数
这三个步骤不能被拆分,中途不能被打断,他说这个叫原子操作。
他给了我们一套原子操作的手册,里面不止有减法,还有加法、位运算,只要调用手册里的原子操作函数,就能保证逻辑的正确性。
我很好奇操作系统大哥是如何实现这个过程的原子化的,他告诉我,如果CPU只有一个核很好办,执行原子操作的时候,他不切换线程就可以。而如果是有多个核,就需要CPU来帮忙了。
你还别说,我们用这个原子操作来卖票后,再也没有发生超额卖票的问题。
自旋锁
有一天,我们卖票程序进行了升级,不再是直接读取票数->票数减1->写回票数
这么简单,还需要安排座位,现在变成了:
我们一翻手册,没有哪个原子操作函数能满足我们的功能,毕竟安排座位这个操作是咱们卖票程序自己的事儿,一点也不通用,操作系统大哥肯定不会专门为我们开发一个原子操作函数。
我们只好再一次求助操作系统大哥,他一看就说:“你们这个问题,用自旋锁就可以解决”
锁?我们还是第一次听说这玩意,不知道是什么意思。
操作系统告诉我们,让我们回去创建一个锁,这锁里面有一个状态标记来表示当前有没有被占用,所有线程在执行卖票操作之前,都得先去获取这个锁,如果锁被占用了,线程就会阻塞在获取函数那里,获取函数内部会不断循环去检查,直到别的线程释放后才返回。
因为获取锁的时候线程会一直循环检查状态,所以这锁也叫自旋锁。
现在,我们的工作流程变成了:
我们又可以愉快地卖票了!
互斥锁
我们的业务发展很快,后来,我们用上了数据库,把票的数量写到了数据库里面,于是我们的工作流程变成了:
本以为只是把票数从本地内存搬到了数据库,应该没什么不一样,结果发现我们运行经常出错,还莫名其妙地被杀掉进程。
我们向操作系统大哥大倒苦水,没想到他却说:“你们还好意思诉苦,你们获取自旋锁后搞那么耗时的操作,让别的线程一直自旋等待,把CPU都跑得飞起,风扇都转个不停···”
我们都羞愧地低下了头,原来,把票数从本地内存搬到了数据库,差别这么大。
操作系统又接着说道:“自旋锁因为会使得线程一直阻塞自旋,没有让出CPU,所以只适合处理比较快速的场合,像读取数据库这种很耗时的操作,不能用它,会白白浪费CPU时间!”
我们又询问:“有没有别的不浪费CPU的办法呢?”
操作系统大哥又给我们介绍了一个叫互斥锁的东西,听说获取这个锁的时候,线程不会去自旋检查,而是把自己放到这把锁的等待队列中,然后就交出CPU执行权限,进入睡眠,看起来就跟阻塞一样,等到后面别的线程释放锁之后,再去唤醒它的等待队列里的线程继续运行。
回去以后我们就用上了互斥锁,现在我们的流程变成了这样:
我们又又能愉快地卖票了!
条件变量
有一天,我们的卖票程序又进行了升级,21-100号票价格比较便宜,交给其他线程来卖,1-20号票价格比较贵,交给我来卖。
现在,我们不同的线程卖的票不一样了。
别的线程的流程是这样:
而我的流程是这样:
使用互斥锁倒也没什么问题,可就是我经常拿到锁以后发现票号还大于20,不该我处理,只好默默的释放锁,白白把我唤醒,却什么也没干!
空手而回的次数多了以后,我又去请教操作系统大哥,能不能让我指定一个条件,等条件满足了再唤醒我运行,别让我白跑。
没想到还真有办法!操作系统告诉了我一个叫条件变量的东西,等待条件变量的线程平时阻塞着,别的线程发现条件满足之后,就将条件变量激活,那个时候等待的线程才会被唤醒。
回去之后,我跟我的小伙伴儿们商量了一下,我们创建了一个条件变量,等到它们发现票号小于等于20的时候,就把条件变量激活,我就会被唤醒,再也不用白跑了!
信号量
互斥锁和条件变量真是好东西,帮了我们大忙,不仅帮我们解决了卖票的问题,我们还在其他很多地方使用它,我们遇到的绝大多数同步和互斥问题都可以用它们来解决。
直到有一天,我们遇到了一个新的问题。
我们的票越卖越好,从100到1000,票的数量越来越多,来找我们买票的客户也越来越多。
因为每次售票都要访问数据库,连接它的线程有些多,那家伙有些吃不消了。
希望我们控制一下访问数据库的线程数量。
我们很自然的想到了互斥锁,只有拿到锁的线程才能去访问数据库。
可这互斥锁名叫互斥,只能允许一个线程拿到锁,总不能只允许一个线程访问数据库吧,那可不行。所以我们希望这个名额能放宽,允许多个线程同时获得锁。
我们再一次找到了操作系统大哥,大哥拿出了他的绝招——信号量。
他告诉我们,这信号量就像一个升级版的互斥锁,它里面有一个计数器,可以用来指定最多允许多少个线程同时获得它。
这正是我们想要的锁!
很快我们用上了信号量,我们又又又能愉快地卖票了!
Tips:在信号量一节中,实际上数据库能承受的并发量远不止这点,这里为了故事情节需要,弱化了数据库的并发承受能力。
往期推荐
一个故事看懂进程间通信技术
月黑风高夜,突然听得咣当一声,Web服务器的目录下冒出了两个文件,弄出了不小的声响。这两个家伙一胖一瘦,鬼鬼祟祟,潜入这台计算机,不知要搞什么名堂。
“二弟,一会儿咱们按照计划好的运行起来,分头行事,你等我信号,拿到数据后赶紧撤”,胖子对瘦子说到。
“老大,这地方我不熟悉,我怎么等你信号?咱们得想个联系方式,一会儿通信使用”,瘦子说到。
“这个不用担心,主人都交代好了”,胖子一边说,一边从背后拿出一本 《Linux进程间通信手册》 翻了起来。
信号
翻开手册的第一页,上面写着:信号——Signal,两个家伙开始认真研究起来。
片刻之后,胖子程序说道:“唉,这个不行,往后翻吧!”
瘦子程序不解,问道:“咋就不行啦?”
“你看这里,手册上说了,信号是Linux上的一种软中断通信机制,可以向指定进程发送通知,总共有64种信号,不过这个信号只能作为通知使用,没办法传输数据”
“没法传输数据?那这玩意有什么用?”
“还是有用啊,可以通知某个进程发生了什么事件,比如kill命令就是利用这个信号来告知进程退出从而实现杀进程的效果的”
“原来如此”,瘦子程序若有所思的点了点头,此时胖子已经翻到了手册的第二页。
socket
“你看,手册上写了,可以用socket”,胖子说到。
“socket?那不是网络通信使用的吗?”,瘦子有些疑惑。
“是啊,不过咱们一样可以用来在本地计算机通信,把连接的IP地址换成127.0.0.1就行了”
“感觉有点太招摇了,咱们计算机内部进程通信,数据还要经过网卡,很容易被发现的!”
“不会不会,手册上说了,127.0.0.1是本地回环地址,数据在协议栈就进行转发了,根本不会到达网卡”
“那抓包能抓到咱们通信吗?”
“嗯,让我看看···手册上说,可以在虚拟的回环网卡lo上抓到数据”
“还是算了吧,咱干这事得悄悄进行,不能留下痕迹,你再看看还有没有别的招”
听瘦子这么一说,胖子倒也觉得有理,便在手册上继续翻了起来。
匿名管道
“哎,有了有了,这个叫匿名管道的,听起来就比较隐秘,应该不会被发现。”
瘦子接过手册,看了起来。
这俩盯着手册上的两个图研究了半天,总算弄明白了,所谓匿名管道不过是内核中的一段缓冲区,提供了读写两个口子,通过fork创建子进程后,子进程继承了父进程的管道信息,两边只要约定好,一个读,一个写,就能实现通信了。
“老大,这匿名管道是单向的,咱们要通信,得整两根管道才行,一个你写我读,一个我写你读”
“看起来挺靠谱,就这么干!”,二人达成了一致。
胖子程序率先运行起来,随后创建了两个管道,一个用来发送消息,一个用来接收消息。接着执行fork,将瘦子程序也运行了起来。
时间过的很快,转眼已是深夜,随着计算机被关掉,两个家伙的进程也都退出了。
半夜无人之际,硬盘中这两个家伙开始吵起来了。
“你是怎么回事?我给你发消息怎么也不回,害得我一连发了一堆消息,最后把管道塞满了,我都阻塞了!”,胖子程序气愤地说道。
“嗨!别提了,主人给我写的程序有bug,今天运行的时候不小心崩溃了,等我再次起来时,发现管道不见了,什么情况啊?”,瘦子程序说完叹了一口气。
“那肯定不行,这匿名管道需要有亲缘关系的进程继承后才能通信,你用别的方式运行起来,肯定看不到我创建的管道啊!”
“这匿名管道用起来太麻烦了,看看还有没有别的通信方式?”
胖子程序又掏出了手册,翻了起来。
消息队列
“有了有了,这里还写了两种方式:命名管道、消息队列”,胖子程序说到。
“命名管道?跟匿名管道有什么区别吗?”
“命名管道有名字,有了名字就不限有亲缘关系的进程才能通信了,只要使用这个名字,都能打开管道通信,这下你就算挂了重启也能跟我联系上了”
“那消息队列又是什么东西?”
“额,让我看一下”
“给我也看看”,瘦子程序凑了上去,一起看了起来。
过了一会儿,瘦子程序说道:“我看明白了,这消息队列是内核中的一个消息链表,按照消息块组织,比那管道全是二进制数据流堆积在一起好用多了”
“有道理,而且这消息还可以指定类型,这样咱们俩就不用弄两个管道,一个消息队列就行了,咱们俩使用不同的消息类型,可省了不少事儿啊!”
“那咱们就用消息队列吧,别用那什么管道了”
“好,就这么干!”
两个家伙一拍即合,准备第二天再大干一场。
第二天,计算机启动后,它们又偷偷的运行了起来。
这一次用上了消息队列,联络起来方便了不少。
共享内存
不知过了多久,那瘦子进程总算来信儿了,胖子从消息队列中取出一看,只见上面写着:
“老大,我拿到了数据了,需要你来处理一下,不过这数据体量有点大,用管道和消息队列传输效率都太低了,有无办法快速把数据传送给你,盼速回。”
胖子进程心里一阵欢喜,数据拿到了,总算可以回去交差了。不过怎么样快速把数据传送过来呢,心里又犯起了嘀咕。
此时,胖子进程又一次拿出手册,翻到了最后一页,发现了一个叫“共享内存”的东西,仿佛像抓住了救命稻草一般,仔细研究了起来。
片刻之后,胖子的脸上露出了笑容,随后写下了一条消息给瘦子进程发送了过去。
却说这瘦子进程正在焦急等待消息中,收到老大的回信后,赶紧取出来看:
二弟,主人的手册中提到,可以使用共享内存进行进程间通信。
我准备了几个内存页面,你将它们映射到你的进程地址空间中,咱们就能共享这一片内存,你写的数据我能立即看到,我写的你也能立即看到,虽然咱们各自读写的地址不同,但实际上是访问的同一片物理内存页面,比管道和消息队列效率高多了!
不过为了防止咱们一起读写发生冲突,需要配合信号量一起使用,用它来实现进程间同步。
具体的使用方法如下:
······
······
盼速回!
瘦子进程看完,心中大喜!赶紧通过消息队列发了一封回信。
随后,通过老大交代的方法开始操作起来,打开共享、映射挂载一气呵成。再接着,将数据一股脑儿写到了共享的内存页面中。
大功告成之后,便退出了进程,按照计划准备撤退,却不见了胖子的踪迹,既无进程也无文件。
“这家伙难道抛下我一个人跑了?”
正想着,突然“嗡”的一声,瘦子的程序文件也没了。
却看那文件目录之下,只留了一卷《Linux进程间通信手册》···
相关推荐
以上是关于趣话计算机底层技术一个故事看懂各种锁的主要内容,如果未能解决你的问题,请参考以下文章