多个线程之间共享虚拟空间地址,也就是很多资源都是共享的(临界资源),这样最大的优点就在于通信方便,同时也存在弊端,那就是缺乏访问控制。两个线程如果访问并修改同一个全局变量,这就容易引发逻辑错误,甚至有可能造成线程崩溃。
至于线程崩溃具体是怎样的,下面从两方面介绍,一方面是站在底层的角度了解,然后另一方面是通过代码测试的结果更直观的看到线程逻辑错误的影响。
目录
一、线程因临界资源产生冲突的情景(感性认识)
现在有一个抢票软件,一共有2个线程在抢票,抢到了则总票数tickets自减,一开始tickets = 1000,在那之前我们需要了解 tickets-- 的过程。
1、了解 tickets-- 的过程(站在内存的角度)
tickets -- 并非像我们认识的那样,直接一步到位。站在OS的角度,实际上分成了三步,转换成汇编语言以后也是分成了至少三步来实现。
第一步,CPU会先把 tickets 的值加载CPU的寄存器上;
第二步,CPU判断 tickets>0 或者 tickets--;
第三步,将修改以后的 tickets 值或者判断结果返回给内存。
2、临界资源冲突情景
现在开始抢票,假设CPU是按照编号顺序调度线程,注意是假设,真实情况不一定。当第 0 号线程,刚好进行完第一步以后,才刚把数据加载到CPU的寄存器上,还没来得及进行第二步,时间片就到了,于是第 0 号线程就拿着上下文数据排到运行队列的队尾(这里的上下文数据指的是tickets的值1000)
下面第1号线程接上,1号线程没有受到什么阻碍,一直在抢票,顺利的把tickets减少到 990 张,第1号线程保存了上下文数据tickets的值,排到队尾。
现在又轮到第0号线程了,CPU先把上下文数据加载到寄存器里,然后再开始进行第二步、第三步,此时上下文的 tickets = 1000,等抢到5张票以后时间片到了,内存中的票数被改成了 995,第0号线程又带着上下文数据 tickets = 995 排到队尾
到这里我们就会发现问题了,第0号线程第二次被调度的时候,把内存中tickets = 990 改成了 tickets = 995,那第1号线程岂不是白干活了??这个时候两个线程就发生了冲突
二、线程资源冲突代码重现(理性认识)
说了一堆理论的,下面我们使用代码来还原上面的过程
1、测试代码及测试结果
电影院售票100张,我们开设5个线程同时开始抢票,抢到了就打印还剩多少张票,这里我们要重点关注的是打印的结果
=====================子线程=====================
=====================主线程=====================
=====================Makefile文件=====================
测试结果如下,我们会发现票数出现了负数!!
2、测试结果分析
关于上面的现象,我们可以这么理解,当线程3进来的时候,此时ticket = 1,正准备自减的时候,相当于正好加载到CPU的寄存器上时,线程3的时间片到了,此时要轮到线程4被CPU调度了
线程4的tickets = 0,正好接上线程3开始自减,自减以后tickets就变成了-1
等到下一次线程4再次遇到,前面一个线程刚进入 if 语句,时间片就到了,此时线程4再次“乱入”,此时就会把线程4 的tickets打印出来 ,此时就出现了 tickets = -1的情况。
三、解决线程“乱入”:为临界区加锁
解决方案放在下面这篇博客中,含代码和测试结果