互斥锁与死锁(linux多线程)

之前我们了解到一些线程的基本知识,线程等待,线程分离啊什么的。现在我们用这些知识简单实现一个火车站抢票的功能。
假设一共有100张票,我们开放4个窗口(线程),让每个窗口进行卖票的功能,每个窗口之间是独立的,他们的任务就是卖完这100张票,每卖一张票,就让总票数-1。

void* ThreadStart(void* arg)
{
   
   (void*)arg;
 
   while(1)
   {
   
     if(g_tickets > 0)
     {
   
       g_tickets--; //总票数-1
       //usleep(100000); //模拟一个窗口的堵塞                                                                                                                                                  
       printf("i am thread [%p],I'll sell ticket number [%d]\n",\
                 pthread_self(),g_tickets + 1);
     }                                  
     else                               
     {
                                     
       break;//没有票了,直接返回该线程 
     }                                  
   }                                    
   return NULL;                         
} 

这样写每个线程的任务,看上去好像是没有什么问题,先看看运行结果
在这里插入图片描述
好像真的没有什么问题,但是这是建立在每个线程执行一个任务都是很快的情况下,我们现实中每一个买票的过程所花费的时间都不短,这可以理解成一种阻塞。我们在程序中模拟一下这个阻塞的过程,看看会出现什么结果。
在这里插入图片描述
不得了了,我们发现好像有几张票没卖出去,又好像1号票被卖了四次,不同的窗口出售了同样的一张票,结果出现了二义性,这个问题很严重,怎么解决呢?这就得用线程安全的知识了

线程安全

通过上面的代码,我们发现多个线程同时运行的时候,在访问临界资源后,使得程序出现了二义性的结果。
线程安全就是为了解决多个线程在同时运行时,在访问临界资源的同时不能让程序出现二义性的结果。

  • 临界资源:在同一时刻,一个资源只能被一个线程(执行流)所访问
  • 访问:在临界区中对临界资源进行非原子操作
  • 原子操作:原子操作使得当前操作是一步完成的,也就是每一个操作只能有两个结果,一个是完成,一个是未完成

再次了解原子性

在这里插入图片描述

  1. 执行流A先从CPU中获得数据 g_tickets = 100,在获取完数据之后发生了阻塞,开始处理这个数据,此时还没有执行 g_tickets-- 的操作。
  2. 在执行流A处理第100张票的同时,执行流B开始从CPU中获取数据,此时因为执行流A还没有进行 g_tickets-- 的操作,CPU中 g_tickets = 100,执行流B开始处理这个数据,进行 g_tickets–,然后返回给CPU处理后的结果,g_tickets = 99。
  3. 之后执行流A在执行 g_tickets-- 之后,返回给CPU的结果是 g_tickets = 99。

经过这样一个模拟阻塞的过程,发现原本应该是 g_tickets = 98 的结果,却因为二义性导致结果是 g_tickets = 99。这就是由于执行流A执行的 g_tickets-- 操作是非原子的操作,也就是执行流A在执行的时候,可能会遇到时间片耗尽,从而导致执行流A被调度。相当于执行流A在执行时的任何一个地方都可能会被打断。

如何保证线程安全

  • 互斥:保证在同一时刻只能有一个执行流访问临界资源,如果多个执行流想要同时问临界资源,并且临界资源没有执行流在执行,那么只能允许一个执行流进入该临界区。
    也就是如果执行流A,B,C…想要同时访问临界资源,这时就只能有一个执行流先访问临界资源,假设此时访问的是执行流A,其他执行流B,C…不能打断执行流A的执行。
  • 同步:保证程序对临界资源的合理访问。也就是执行流A执行完自己的任务时,必须让出CPU资源,不能一直占着CPU,应该及时让出CPU,使其他执行流也可以执行他们自己的任务。

在这里插入图片描述
看着图片“形象”的再来理解一次,假设有一个厕所,互斥就是同一时间只能有一个滑稽去上厕所,其他滑稽只能在外面排队;同步就是滑稽A上完厕所后不能占着茅坑不拉* ,应该赶紧出去让出坑位给其他滑稽。

想要做到这几点,本质上就是需要一把锁,也就是互斥量 (mutex)
在这里插入图片描述

互斥锁

互斥锁是用来保证互斥属性的一种操作
互斥锁的底层是互斥量,而互斥量**(mutex)**的本质是一个计数器,这个计数器只有两个状态,即 0 和 1 。

  • 0:表示无法获取互斥锁,也就是需要访问的临界资源不可以被访问
  • 1:表示可以获取互斥锁,也就是需要访问的临界资源可以被访问

在这里插入图片描述

加锁与解锁

加锁的过程可以使用互斥锁提供的接口,以此来获取互斥锁资源

  • 互斥量计数器的值为1,表示可以获取互斥锁,获取到互斥锁的接口就正常返回,然后访问临界资源。
  • 互斥量计数器的值为0,表示不可以获取互斥锁,当前获取互斥锁的接口进行阻塞等待。

加锁操作:对互斥锁当中的互斥量保存的计数器进行减1操作
解锁操作:对互斥锁当中的互斥量保存的计数器进行加1操作

看到这里,就可以简单的理解为加锁和解锁就是这样的一个过程在这里插入图片描述
那么问题来了,我们在购票代码中改变票数的操作就是 g_tickets–,这个的本质就是减一。互斥量计数器本身也是一个变量,这个变量的取值是0/1,对于这样一个变量进行加一减一操作的时候,这就是原子性操作吗?

这时候不禁想起老爹的那句话
在这里插入图片描述
所以说,想要解决原子性的问题,还得要用原子性的操作,怎么一步就判断有没有加锁呢?
在这里插入图片描述
汇编中有一个指令xchgb,可以用来交换寄存器和内存中的内容,这个操作就是原子性的,一步到位。
我们再来分析一下,如果是需要加锁的情况,互斥量计数器最后就会从1变成0;如果是不能加锁的情况,互斥量计数器中的值还是0,也就是判断之

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值