Linux下线程的同步与互斥以及死锁问题整理

来源:互联网 发布:为什么淘宝免运费 编辑:程序博客网 时间:2024/05/29 10:46

前言:

在同一个进程中的全局变量不同的线程是能够看到的。之前我们在进程中了解到#####二元信号量,是用来保护临界资源,代码上保护临界区,在两个不同的进程在访问公共资源的时候保证访问的互斥和同步。
那么在线程中,不同的线程共享进程地址空间,为了保护公共的数据,我们也必须要保证线程的互斥和同步。

测试用例:

在一个进程中定义一个全局变量,让两个线程同时对這个公共资源进行累加(累加的次数很大为了看到明显的效果),我们观察现象。
我们知道对一个变量的算数计算不是原子的(分为三步)。
测试代码:主线程和新线程对一个全局变量同时累加5000000次,观察结果

这里写图片描述

运行几次的结果:

这里写图片描述

我们发现在几次的运算之后,有些结果并非符合我们的预期,也就是说這里面有错误,也就是在某一个线程在访问临界资源(count)进行累加的时候,這个线程可能因为调度优先级的原因或者受到信号需要从用户层切换到系统层(系统调用)的时候被切出去了,那么這个时候访问這个count临界资源的可能就是其他线程了,這样就造成了计算结果的错误。
问题:两个线程在什么时候切换的?当有线程发生从用户态到内核态的切换的时候,比如有系统调用的时候,一个线程进入内核态,然后去做其他事情了,這个时候其他线程就会访问count:问题用户态和内核态的概念;
一个变量的累加就必须要进行内核态和用户态的切换,为什么呢? 哪些情况下线程会发生从用户态和内核态的切换?
(linux基本概念笔记模块里面有)
复习和补充:全局变量线程间资源共享(当然局部变量是存放在私有栈上面的)
新线程和主线程有哪些东西是共享的?(代码、静态全局区,只读全局区,堆(因为malloc之后系统是把资源给进程而不是线程,这是属于资源分配范畴),环境变量、命令行参数)。
对于线程能看到的临界资源,对临界资源的操作必须保证原子性以保护临界资源。
什么叫互斥:一个线程在访问临界资源的时候是独享的,其他线程是不能访问的。同一时间只有一个线程占有临界资源。
什么叫同步:不同的线程按照一种特定的顺序访问临界资源(放完了才能拿苹果,拿完了才能放)。同步机制建立在互斥之上。
互斥锁:实现互斥。,与二元信号量有点相似。
互斥锁的目的:为了保证两个线程在访问同一个临界资源的时候不会对对方产生影响。
定义一把全局的锁可以使用宏:PTHREAD_MUTEX_INITIALIZER;

接口:

1.创建锁

include <pthread.h>pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER; 
说明:如果mutex是静态分配的,是全局变量或者static修饰的,可以使用宏进行初始化。
int pthread_mutex_init(pthread_mutex_t *restrict mutex, const pthread_mutexattr_t *restrict attr);int pthread_mutex_destroy(pthread_mutex_t *mutex);
说明:初始化一把锁和销毁一把锁,对于初始化接口里面的attr可以使用NULL生成缺省的。
注意:使用宏进行初始化和使用init进行初始化是一个意思,最后都需要进行destory。
返回值:成功返回0,失败返回错误码。

2.加锁和解锁

include <pthread.h>int pthread_mutex_lock(pthread_mutex_t *mutex);int pthread_mutex_trylock(pthread_mutex_t *mutex);int pthread_mutex_unlock(pthread_mutex_t *mutex);
说明:一个线程可以调⽤用pthread_mutex_lock获得Mutex,如果这时另一个线程已经调⽤用 pthread_mutex_lock获得了该Mutex,则当前线程需要挂起等待,直到另一个线程调⽤用 pthread_mutex_unlock释放Mutex,当前线程被唤醒,才能获得该Mutex并继续执⾏行。
参数:都是把锁变量的地址传进去(传址调用)。
如果一个线程既想获得锁,又不想挂起等待,可以调⽤用pthread_mutex_trylock,如果Mutex已 经被 另⼀一个线程获得,这个函数会失败返回EBUSY,⽽而不会使线程挂起等待。(可以使用循环实现一个轮询的申请锁资源)。

互斥锁如何实现互斥:

和二元信号量实际上差不多,假设开始的时候锁的值为1,如果该锁被某一线程申请,那么就置锁为0,如果锁的值为0,其它线程调用lock会挂起等待,当线程访问临界空间结束了之后吊影unlock解锁,则将锁的置1,那么被挂起的线程就被唤醒。
“挂起等待”和“唤醒等待线程”的操作如何实现:每个Mutex有一个等待队列,一个线程要在Mutex上挂起等待,⾸首先在把⾃自⼰己加⼊入等待队列中,然后置线程状态为睡眠,然后调⽤用调度器函数切换到别的线程。一个线程要唤醒等待队列中的其它线程,只需从等待队列中取出一项,把它的状态从睡眠改为就绪,加⼊入就绪队列,那么下次调度器函数执⾏行时就有可能切换到被唤醒的线程。
为什么说加锁和解锁操作一定要是原子的?:首先要明确互斥锁也是一份临界资源,互斥锁想要保护临界资源首先要将自己保护好,而将自己保护好的措施就是让线程在操作的时候保证原子操作。要对临界资源保护就必须保证对临界资源操作的原子性。
重要问题:初始化一把锁可以不是原子的,对于一把锁的加锁的解锁一定是原子操作;但是对于变量的运算操作必定不是原子操作,如何保证加锁和解锁的原子性:
为了实现互斥锁操作,大多数体系结构都提供了swap或exchange指令,该指令的作用是把寄存器和内存单元的数据相交换,由于只有一条指令,保证了原子性,即使是多处理器平台,访问内存的总线周期也有先后,一个处理器上的交换指令执行时另一个处理器的交换指令只能等待总线周期。
补充:一个线程拿着锁的时候是可以被切出去的,但是注意时拿着锁出去的,其他的线程想要加锁操作就没法申请,这和进程一样属于独占式的占据资源。
示例代码:

这里写图片描述


死锁问题

典型场景:线程A获得了锁1,线程B获得了锁2,这时线程A调⽤用lock试图获得锁2,结果是需要挂起等待线程B释放锁2,而这时线程B也调⽤用lock试图获得锁1,结果是需要挂起等待线程A释放锁1,于是线程A和B都永远处于挂起状态了;这就叫做死锁(DeadLock)
对于进程而言(进程死锁): 如果一组进程中的每一个进程都在等待仅由该组进程中的其它进程才能引发的事件,那么该组进程是死锁的。
图示:

这里写图片描述


产生死锁的四个必要条件:

(1)互斥条件:一个资源每次只能被一个线程使用。
(2)占有且等待:一个线程因请求资源而阻塞时,对已获得的资源保持不放。
(3)不可强行占有:线程已占有的资源,在末使用完之前,不能强行剥夺。
(4)循环等待条件:若干线程之间形成一种头尾相接的循环等待资源关系,等待队列。

如何避免死锁问题:

实际在写代码的过程中:1.避免同时获得多个锁资源,如果一定要這样做则必须要遵循原则:如果所有线程在需要多个锁时都按相同的先后顺序(常见的是按Mutex变量的地址顺序)获得锁,则不会出现锁。
2.尽量使⽤用pthread_mutex_trylock调⽤用代替 pthread_mutex_lock 调用,以免死锁。因为trylock要是申请不到不会把线程挂起。

一些避免死锁问题的算法:

1.银行家算法:

设线程i提出请求Request[j],则银行家算法按如下规则进行判断。
(1) 如果Request[j]≤Need[i,j],则转向(2),否则认为出错,因为它所需要的资源数已超过它所宣布的最大值。
(2) 如果Request[j]≤Available[j],则转向(3);否则表示尚无足够资源,Pi需等待。
(3) 假设进程i的申请已获批准,于是修改系统状态:
Available[j]=Available[j]-Request[i]
Allocation[i,j]=Allocation[i,j]+Request[j]
Need[i,j]=Need[i,j]-Request[j]
(4)系统执行安全性检查,如安全,则分配成立;否则试探险性分配作废,系统恢复原状,进程等待。

2.安全性算法:

(1) 设置两个工作向量Work=Available;Finish[i]=False
(2) 从进程集合中找到一个满足下述条件的进程,
Finish [i]=False;
Need[i,j]≤Work[j];
如找到,执行(3);否则,执行(4)
(3) 设进程获得资源,可顺利执行,直至完成,从而释放资源。
Work[j]=Work[j]+Allocation[i,j];
Finish[i]=True;
Go to step 2;
(4) 如所有的进程Finish[i]=true,则表示安全;否则系统不安全。

如何解除死锁:

两种方案:
(1)抢占资源:从其他的线程中抢占资源分配给死锁线程以释放死锁状态。
(2) 终止线程:之前有学习了如何让一个线程退出,可以让进程中的一个或者多个线程退出,打破循环环路,让系统从死锁状态中解脱出来。
原创粉丝点击