你的线程真的安全吗?—— 线程安全与锁机制

问题引入

上篇我们谈论了线程的基本的概念,线程控制原语,以及多线程与多进程之间的区别。提到进程我们就不得不说线程,提到线程那么势必就会谈及线程安全 ,因为它非常的重要。为什么这么说呢?我们举个例子,如下:

举例:
   有一位大婶去银行存款5000元 。卡里本来余额还有5000元,银行小姐姐查询余额后发现还有5000元,这时候银行小姐姐要将大婶的5000元改成10000元,就在这时候大婶眼疾手快,立马去ATM上取款3000元,但是银行的小姐姐不知道,依然将大婶的5000元改为10000元,这位大婶就这样发家致富了,哈哈,其实这只是个笑话而已。大家千万不能当真,在现实生活中,这种情况是不可能发生的,因为银行早在你能想到之前就已经避免发生这种“错误故此,我们在完成线程的相关操作时,线程安全就显得尤为重要。

线程安全

  1. 概念:多个线程同时操作临界资源而不会出现二义性,我们就称这个线程是安全的。(临界资源:多线程执行流共享的资源)
    注:线程安全的关键点就在于我们是否对临界资源进行了非原子操作,导致线程可能出现不安全的情况。(原子操作:不会被任何调度机制打断。)
  2. 如何实现线程安全? 采用同步与互斥
    (1)同步:线程同步,指一个线程发出某一功能调用时,在没有得到结果之前,该调用不返回。同时其它线程为保证数据一致性,不能调用该功能。简单来说同步就是保证对临界资源访问的合理性。
    (2)互斥:线程互斥,任何时刻互斥保证有且只有一个执行流进入临界区,访问临界资源,通常对临界资源起保护作用。简单来说互斥就是保证了临界资源在同一时间的唯一访问性。

如何实现线程互斥

互斥锁

  1. 互斥锁概念:在Linux下提供了一种锁叫互斥锁(mutex),也叫互斥量,每个线程对资源访问前都尝试加锁,加锁成功后才能访问数据,操作完后进行解锁。虽然资源还是共享的,线程之间还是竞争的,但是通过锁机制就能保证临界资源的唯一访问性。
  2. 互斥锁本质:互斥锁其实就是一个0/1计数器,1表示可以加锁,加锁就是让计数器减1,操作之后要进行解锁,解锁就是计数器加1,0表示不可以加锁,不能加锁则等待。

在这里插入图片描述
  当A线程对某个全局变量加锁访问,B在访问前尝试加锁,拿不到锁,B阻塞。C线程不去加锁,而直接访问该全局变量,依然能够访问,但会出现数据混乱。所以,互斥锁实质上是操作系统提供的一把“建议锁”(又称“协同锁”),建议程序中有多线程访问共享资源的时候使用该机制。但并没有强制限定。因此,即使有了mutex,如果有线程不按规则来访问数据,依然会造成数据混乱。

互斥锁操作步骤

  1. 定义互斥锁变量
    在这里插入图片描述
  2. 初始化互斥锁变量
    在这里插入图片描述
  3. 加锁
    在这里插入图片描述
  4. 解锁
    在这里插入图片描述
  5. 销毁互斥锁
    在这里插入图片描述

互斥锁的简单应用

~~黄牛抢票程序

  1 #include <stdio.h>
  2 #include <stdlib.h>
  3 #include <unistd.h>
  4 #include <pthread.h>
  5 #include <errno.h>
  6 
  7 //互斥锁简单应用
  8 //黄牛抢票程序
  9 
 10 
 11 //定义票的数量
 12 int ticket = 100;
 13 
 14 //定义互斥锁变量
 15 pthread_mutex_t mutex;
 16 
 17 //线程入口函数
 18 void* thr_start(void* args){
 19     while(1){
 20         //加锁要在临界资源访问之前
 21         pthread_mutex_lock(&mutex);
 22         if(ticket > 0){
 23             //sleep(1);
 24             printf("我是黄牛%d,我正在抢票%d~~\n",*(int*)args,ticket);
 25             ticket--;
 26         }
 27         else{
 28             //在线程任意有可能退出的地方解锁
 29             pthread_mutex_unlock(&mutex);
 30             pthread_exit(NULL);
 31         }
 32 
 33         pthread_mutex_unlock(&mutex);
 34     }
 35     return NULL;
 36 }
 37 
 38 
 39 int main(){
 40 
 41     //定义黄牛个数
 42     pthread_t tid[4];
 43     int i , ret;
 44     //初始化互斥锁变量
 45     pthread_mutex_init(&mutex,NULL);
 46 
 47     //创建线程,让黄牛抢票
 48     for(i = 0; i < 4; i++){
 49         ret = pthread_create(&tid[i],NULL,thr_start,(void*)&i);
 50         if(ret != 0){
 51             printf("yellowbull is not exist!\n");
 52             return -1;
 53         }
 54     }
 55 
 56     //线程等待,回收资源
 57     for(i = 0 ; i < 4; i++){
 58         pthread_join(tid[i],NULL);
 59     }
 60     //销毁互斥锁变量
 61 
 62     pthread_mutex_destroy(&mutex);
 63 
 64     return 0;
 65 }


死锁

  1. 概念:死锁是指在一组进程(线程)中,每个进程都占有不会释放的资源,但因相互请求被其它进程所占用的资源而导致程序处于一种永久等待的状态。
  2. 线程死锁的情形
    (一):线程试图对同一个互斥量A加锁两次。在这里插入图片描述
    (二):线程1拥有A锁,请求获得B锁;线程2拥有B锁,请求获得A锁

在这里插入图片描述
死锁原因

  1. 死锁产生的四个必要条件
    互斥条件:一个资源每次只能被一个执行流使用
    请求与保持条件:一个执行流因请求资源而阻塞时,对已获得的资源保持不放
    不可剥夺条件:一个执行流已获得的资源,在末使用完之前,不能强行剥夺
    循环等待条件:若干执行流之间形成一种头尾相接的循环等待资源的关系

  2. 如何避免死锁
    破坏死锁的必要条件
    加锁顺序一致
    避免死锁未释放的场景
    资源一次性分配

死锁避免算法

  1. 银行家算法:银行家算法(Banker’s Algorithm)是一个避免死锁(Deadlock)的著名算法,是由艾兹格·迪杰斯特拉在1965年为T.H.E系统设计的一种避免死锁产生的算法。它以银行借贷系统的分配策略为基础,判断并保证系统的安全运行。一个银行家如何将一定数目的资金安全地借给若干个客户,使这些客户既能借到钱完成要干的事,同时银行家又能收回全部资金而不至于破产,这就是银行家问题。这个问题同操作系统中资源分配问题十分相似:银行家就像一个操作系统,客户就像运行的进程,银行家的资金就是系统的资源。
  2. 安全序列是指一个进程序列{P1,…,Pn}是安全的,即对于每一个进程Pi(1≤i≤n),它以后尚需要的资源量不超过系统当前剩余资源量与所有进程Pj (j < i )当前占有资源量之和。
  3. 算法原理:我们可以把操作系统看作是银行家,操作系统管理的资源相当于银行家管理的资金,进程向操作系统请求分配资源相当于用户向银行家贷款。 为保证资金的安全,银行家规定:
      当一个顾客对资金的最大需求量不超过银行家现有的资金时就可接纳该顾客;
      顾客可以分期贷款,但贷款的总数不能超过最大需求量;
      当银行家现有的资金不能满足顾客尚需的贷款数额时,对顾客的贷款可推迟支付,但总能使顾客在有限的时间里得到贷款;
      顾客得到所需的全部资金后,一定能在有限的时间里归还所有的资金.
      操作系统按照银行家制定的规则为进程分配资源,当进程首次申请资源时,要测试该进程对资源的最大需求量,如果系统现存的资源可以满足它的最大需求量则按当前的申请量分配资源,否则就推迟分配。当进程在执行中继续申请资源时,先测试该进程本次申请的资源数是否超过了该资源所剩余的总量。若超过则拒绝分配资源,若能满足则按当前的申请量分配资源,否则也要推迟分配。

死锁检测与恢复
   一般来说,由于操作系统有并发共享以及随机等特性,通过预防以及避免死锁的方法是很难解决死锁问题的,需要较大的系统开销,不能充分利用资源。为此,我们往往采用一种简单的方法去解决这个问题,这个方法不采取任何限制性的手段,但是它提供了死锁的检测与恢复的方法。当发生死锁问题时,它能很快的检测到死锁问题,并且能从死锁状态中恢复出来,因此在实际当中操作系统往往采用死锁的检测与恢复的方法来解决死锁问题。

死锁检测算法
  什么是死锁检测算法?下面我给大家介绍一个比较简单易懂的死锁检测算法。它是给每个进程和每个资源都设定了一个编号,然后建立两个数据结构,一个是资源分配表,它用来存储每个资源分配给每个进程的关系,一个是进程等待表,它是用来保存各个进程与它要等待资源之间的一个关系。这个算法就是通过这两个表之间的关系来判断线程是否处于死锁状态。下面我通过一个例子具体的解释一下这个算法的实现过程。
  假如,我们现在进程等待表中选取进程P1,发现它在等待资源Z1,然后我们再去资源分配表中查找资源Z1的分配情况,假如资源Z1分配给了进程P2,然后我们判断一下P2进程是否处于等待状态,如果它不处于等待状态,判断结束,假如它也处于等待状态,我们将要在进程等待表中查看P2等待哪个资源,假如P2进程等待资源Z2,Z2资源又分配给了P3进程,然后做与P2进程同样的操作,假如P3进程也在等待资源,它等待资源Z3,恰好资源Z3又分配给了进程P1,咦,这是不是就尴尬了,出现环路了。。其实P1在等自己,那不是凉凉了,肯定就无休止的等待下去了。。通过这种方式我们就检测出是否有死锁发生。如图所示
 在这里插入图片描述

死锁恢复
   (1)最简单最常用的方法就是进行系统的重新启动,不过这种方法代价很大,它意味着在这之前所有的进程已经完成的计算工作都将付之东流,包括参与死锁的那些进程,以及未参与死锁的进程。
   (2)撤销进程,剥夺资源。终止参与死锁的进程,收回它们所占有的资源,从而解除死锁。这时又分两种情况:一次性撤销参与死锁的全部进程,剥夺全部资源;或者逐步撤销参与死锁的进程,逐步收回死锁进程占有的资源。一般来说,选择逐步撤销的进程时要按照一定的原则进行,目的是撤销那些代价最小的进程,比如按进程的优先级确定进程的代价;考虑进程运行时的代价和与此进程相关的外部作业的代价等因素。
  此外,还有进程回退策略,即让参与死锁的进程回退到没有发生死锁前某一点处,并由此点处继续执行,以求再次执行时不再发生死锁。虽然这是个较理想的办法,但是操作起来系统开销极大,要有堆栈这样的机构记录进程的每一步变化,以便今后的回退,有时这是无法做到的。

  • 7
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值