3

【趣话计算机底层技术】一个故事看懂各种锁 - 轩辕之风

 1 year ago
source link: https://www.cnblogs.com/xuanyuan/p/17407494.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

【趣话计算机底层技术】一个故事看懂各种锁

我是一个线程,一个卖票程序的线程。

自从我们线程诞生以来,同一个进程地址空间里允许有多个执行流一起执行,效率提升的同时,也引来了很多麻烦。

我们卖票线程的工作很简单,比如票的总数是100,每卖一张就减1,直到变成0售完为止。

以前单线程的时候没啥问题,但多个线程一起执行的时候就发现,有些家伙读取到票数是100,减1后变成99,还没等他把票数写回去,另外有别的线程去读也是100,也做了同样的事,结果卖了两张票,票数才减了1张,一天下来,多卖了很多票,气的人类差点想砸了我们。

我们把这问题反馈给了操作系统大哥,他给我们的解决方案是:读取票数->票数减1->写回票数这三个步骤不能被拆分,中途不能被打断,他说这个叫原子操作

659280-20230517090249585-1839578963.png

他给了我们一套原子操作的手册,里面不止有减法,还有加法、位运算,只要调用手册里的原子操作函数,就能保证逻辑的正确性。

我很好奇操作系统大哥是如何实现这个过程的原子化的,他告诉我,如果CPU只有一个核很好办,执行原子操作的时候,他不切换线程就可以。而如果是有多个核,就需要CPU来帮忙了。

你还别说,我们用这个原子操作来卖票后,再也没有发生超额卖票的问题。

有一天,我们卖票程序进行了升级,不再是直接读取票数->票数减1->写回票数这么简单,还需要安排座位,现在变成了:

659280-20230517090259172-1903307968.png

 我们一翻手册,没有哪个原子操作函数能满足我们的功能,毕竟安排座位这个操作是咱们卖票程序自己的事儿,一点也不通用,操作系统大哥肯定不会专门为我们开发一个原子操作函数。

我们只好再一次求助操作系统大哥,他一看就说:“你们这个问题,用自旋锁就可以解决”

锁?我们还是第一次听说这玩意,不知道是什么意思。

操作系统告诉我们,让我们回去创建一个锁,这锁里面有一个状态标记来表示当前有没有被占用,所有线程在执行卖票操作之前,都得先去获取这个锁,如果锁被占用了,线程就会阻塞在获取函数那里,获取函数内部会不断循环去检查,直到别的线程释放后才返回。

659280-20230517090308726-1669911117.png

 因为获取锁的时候线程会一直循环检查状态,所以这锁也叫自旋锁。

现在,我们的工作流程变成了:

659280-20230517090316487-1434900749.png

我们又可以愉快地卖票了!

我们的业务发展很快,后来,我们用上了数据库,把票的数量写到了数据库里面,于是我们的工作流程变成了:

659280-20230517090324950-1332836233.png

 本以为只是把票数从本地内存搬到了数据库,应该没什么不一样,结果发现我们运行经常出错,还莫名其妙地被杀掉进程。

我们向操作系统大哥大倒苦水,没想到他却说:“你们还好意思诉苦,你们获取自旋锁后搞那么耗时的操作,让别的线程一直自旋等待,把CPU都跑得飞起,风扇都转个不停···”

我们都羞愧地低下了头,原来,把票数从本地内存搬到了数据库,差别这么大。

操作系统又接着说道:“自旋锁因为会使得线程一直阻塞自旋,没有让出CPU,所以只适合处理比较快速的场合,像读取数据库这种很耗时的操作,不能用它,会白白浪费CPU时间!”

我们又询问:“有没有别的不浪费CPU的办法呢?”

操作系统大哥又给我们介绍了一个叫互斥锁的东西,听说获取这个锁的时候,线程不会去自旋检查,而是把自己放到这把锁的等待队列中,然后就交出CPU执行权限,进入睡眠,看起来就跟阻塞一样,等到后面别的线程释放锁之后,再去唤醒它的等待队列里的线程继续运行。

回去以后我们就用上了互斥锁,现在我们的流程变成了这样:

659280-20230517090334043-38159072.png

 我们又又能愉快地卖票了!

有一天,我们的卖票程序又进行了升级,21-100号票价格比较便宜,交给其他线程来卖,1-20号票价格比较贵,交给我来卖。

现在,我们不同的线程卖的票不一样了。

别的线程的流程是这样:

659280-20230517090342285-38792857.png

 而我的流程是这样:

659280-20230517090348156-1821560630.png

 使用互斥锁倒也没什么问题,可就是我经常拿到锁以后发现票号还大于20,不该我处理,只好默默的释放锁,白白把我唤醒,却什么也没干!

空手而回的次数多了以后,我又去请教操作系统大哥,能不能让我指定一个条件,等条件满足了再唤醒我运行,别让我白跑。

没想到还真有办法!操作系统告诉了我一个叫条件变量的东西,等待条件变量的线程平时阻塞着,别的线程发现条件满足之后,就将条件变量激活,那个时候等待的线程才会被唤醒。

回去之后,我跟我的小伙伴儿们商量了一下,我们创建了一个条件变量,等到它们发现票号小于等于20的时候,就把条件变量激活,我就会被唤醒,再也不用白跑了!

互斥锁和条件变量真是好东西,帮了我们大忙,不仅帮我们解决了卖票的问题,我们还在其他很多地方使用它,我们遇到的绝大多数同步和互斥问题都可以用它们来解决。

直到有一天,我们遇到了一个新的问题。

我们的票越卖越好,从100到1000,票的数量越来越多,来找我们买票的客户也越来越多。

因为每次售票都要访问数据库,连接它的线程有些多,那家伙有些吃不消了。

希望我们控制一下访问数据库的线程数量。

我们很自然的想到了互斥锁,只有拿到锁的线程才能去访问数据库。

可这互斥锁名叫互斥,只能允许一个线程拿到锁,总不能只允许一个线程访问数据库吧,那可不行。所以我们希望这个名额能放宽,允许多个线程同时获得锁。

我们再一次找到了操作系统大哥,大哥拿出了他的绝招——信号量

他告诉我们,这信号量就像一个升级版的互斥锁,它里面有一个计数器,可以用来指定最多允许多少个线程同时获得它。

这正是我们想要的锁!

很快我们用上了信号量,我们又又又能愉快地卖票了!

Tips:在信号量一节中,实际上数据库能承受的并发量远不止这点,这里为了故事情节需要,弱化了数据库的并发承受能力。


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK