Linux-----线程互斥量与死活锁

来源:互联网 发布:php curl post提交 编辑:程序博客网 时间:2024/06/05 17:41

一、为什么需要互斥量

 大部分情况下,线程使用的数据都是局部变量,变量的地址在线程栈的地址空间内,这种情况下,变量归属于单个线程,其他线程无法获取到这种变量,如果所有的变量都如此,将会省去无数麻烦,但实际情况是,很多变量都是线程共享的,这样的变量称为共享变量,可以通过数据的共享,完成多个线程之间的交互。 但多个线程并发的操作共享变量,会带来一系列问题 eg:在不加修饰的情况下,利用两个线程对count进行++处理,每个线程分别自加1001次

对count++进行的操作:
1. 从内存读变量值到寄存器
2. 寄存器的值加1
3. 将寄存器的值写回内存

这里写图片描述

没有互斥量情况:

这里写图片描述

有互斥量的情况:

这里写图片描述

//直接对count 进行操作,(保持了原子性)不需要中间变量的情况:

这里写图片描述

这里写图片描述
上面的代码表明,应该避免多个线程同时操作共享变量,对于共享变量的访问,包括读取和写入,都必须限制为每次只有一个线程来操作

1、代码必须要有互斥的行为:当一个线程正在临界区中执行时,不允许其他线程进入该临界区2、如果多个线程同时需要执行临界区的代码并且当前临界区并没有线程在执行,那么只能允许一个线程入进入临界区3、如果线程不在临界区中执行,那么该线程不能阻止其他线程进入临界区
//互斥量的接口1、互斥量的初始化①:将PTHREAD_MUTEX_INITIALIZER赋值给定义的互斥量#include <pthread.h>pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;//如果互斥量为动态分布的,或者需要设定互斥量的属性,则不适合使用②:动态初始化int pthread_mutex_init(pthread_mutex_t *restrict mutex,const pthread_mutexattr_t* restrict attr)//第二个参数是用来设置互斥量的属性的,一般为NULL,调用函数之后,互斥量是处于没有加锁的状态2、互斥量的销毁//销毁时注意事项:①:使用PTHREAD_MUTEX_INITIALIZER初始化的互斥量不需要销毁②:不要销毁一个已加锁的互斥量,或者是真正配合条件变量使用的互斥量③:已经销毁的互斥量,要确保后面不会有线程再尝试加锁int pthread_mutex_destroy (pthread_mutex_t * mutex)//当互斥量处于已加锁状态,或者正在和条件变量配合使用,调用pthread_mutex_destroy函数会返回EBUSY错误码3、互斥量的加锁与解锁int pthread_mutex_lock(pthread_mutex_t *mutex);//可能出现的情况:①、互斥量处于未加锁状态,该函数会将互斥量锁定,同时返回成功②、发起函数调用时,其他函数已锁定互斥量,或者存在其他线程正在申请互斥量,但没有竞争到互斥量,pthread_lock函数的调用会陷入阻塞,等待互斥量解锁//在等待过程中,如果互斥量持有线程解锁互斥量,则可能出现以下情况:函数调用是唯一等待者,获得互斥量,成功返回函数调用线程不是唯一等待者,但成功获得互斥量,返回函数调用线程不是唯一等待者,没有获得互斥量,继续阻塞等待,等待下一轮     int pthread_mutex_trylock(pthread_mutex_t* mutex);//非阻塞式等待,不断地尝试去询问是否可以申请到int pthread_mutex_unlock(pthread_mutex_t* mutex);4、互斥量的缺点①、对互斥量的加锁与解锁操作,本来就有一定的开销②、临界区的代码不能并发执行③、进入临界区的次数过于频繁,线程之间对临界区的争夺过于激烈,若线程竞争互斥量失败,会陷入阻塞,让出CPU,所以执行上下文切换的次数要远远多于不使用互斥量的版本

二、死锁与活锁

对于互斥量而言,可能引发的问题就是死锁(dead lock)

①、线程一已经成功拿到了互斥量1,正在申请互斥量2,而同时在另一个CPU上,线程二已经拿到了互斥量2,正在申请互斥量1,彼此占有对方正在申请的互斥量,结局就是谁也没有办法拿到想要的互斥量,于是就出现了死锁状态②、同一个线程两次调用pthread_lock函数,在第一次调用时,已经申请到了互斥量,第二次调用时,由于互斥量已被占用,会挂起等待锁的释放,又因为自己挂起而没有机会释放锁,就会永久处于挂起状态,形成死锁
//产生死锁的原因主要是:1) 因为系统资源不足。(2) 进程运行推进的顺序不合适。(3) 资源分配不当等。如果系统资源充足,进程的资源请求都能够得到满足,死锁出现的可能性就很低,否则就会因争夺有限的资源而陷入死锁。其次,进程运行推进顺序与速度不同,也可能产生死锁。//产生死锁的四个必要条件:1) 互斥条件:一个资源每次只能被一个进程使用。(2) 请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放。(3) 不剥夺条件:进程已获得的资源,在末使用完之前,不能强行剥夺。(4) 循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系。这四个条件是死锁的必要条件,只要系统发生死锁,这些条件必然成立,而只要上述条件之一不满足,就不会发生死锁。//处理死锁的策略1、忽略该问题。例如鸵鸟算法。2、检测死锁并且恢复。3、仔细地对资源进行动态分配,以避免死锁。4、通过破除死锁四个必要条件之一,来防止死锁产生。//鸵鸟算法:该算法可以应用在极少发生死锁的的情况下。为什么叫鸵鸟算法呢,因为传说中鸵鸟看到危险就把头埋在地底下,可能鸵鸟觉得看不到危险也就没危险了吧。跟掩耳盗铃有点像。//银行家算法:        所谓银行家算法,是指在分配资源之前先看清楚,资源分配后是否会导致系统死锁。如果会死锁,则不分配,否则就分配。按照银行家算法的思想,当进程请求资源时,系统将按如下原则分配系统资源:(1) 当一个进程对资源的最大需求量不超过系统中的资源数时可以接纳该进程。(2) 进程可以分期请求资源,当请求的总数不能超过最大需求量。(3) 当系统现有的资源不能满足进程尚需资源数时,对进程的请求可以推迟分配,但总能使进程在有限的时间里得到资源。(4) 当系统现有的资源能满足进程尚需资源数时,必须测试系统现存的资源能否满足该进程尚需的最大资源数,若能满足则按当前的申请量分配资源,否则也要推迟分配。

在多个线程程序中,如果存在多个互斥量,死锁出现的概率越高
为了避免死锁的出现,还有就是按照一定的先后顺序去申请这些互斥量,就如每个线程都先去申请互斥量1,再去申请互斥量2………让所有的线程都必须遵循同样的顺序来申请互斥量

还有就是通过pthread_trylock()函数尝试,如果拿不到锁就立即返回EBUSY错误码
但是pthread_trylock函数也可能引发活锁(live lock)

//活锁生活中经常遇到两个人迎面走来,双方都想给对方让路,但是让的方向却不协调,反而互相堵住的情况,活锁和这种情况差不多线程1 首先申请了锁1,之后尝试申请锁2,失败后释放锁1进入下一轮循环,同时线程2会因为尝试申请锁1失败而释放锁2,如果恰好两个线程一直保持这种节奏,就可能在很长的时间内都一次次擦肩而过//代码如下://线程一void fun1(){  int done = 0;  while(!done)  {     pthread_mutex_lock(&mutex_a);     if(pthread_mutex_trylock(&mutex_b))       {           count++;           pthread_mutex_unlock(&mutex_b);           pthread_mutex_unlock(&mutex_a);            done = 1;       }       else           pthread_mutex_unlock(&mutex_a);  }}//线程2void fun2(){  int done = 0;  while(!done)  {     pthread_mutex_lock(&mutex_b);     if(pthread_mutex_trylock(&mutex_a))       {           count++;           pthread_mutex_unlock(&mutex_a);           pthread_mutex_unlock(&mutex_b);            done = 1;       }       else           pthread_mutex_unlock(&mutex_b);  }}