Linux--线程死锁

来源:互联网 发布:创业软件 俞青 编辑:程序博客网 时间:2024/06/06 02:22

线程为什会死锁??“锁”又是什么东西?我们这篇博客主要讲一下为什么要给线程加锁,为什么会出现线程死锁,线程死锁怎么解决。

互斥锁

在我的上篇博客已经讲解了一些线程的基本知识Linux–线程控制我们可以了解到线程是共享同一份内存的。这就意味着多个线程同时访问共享数据时可能发生冲突。

分析程序


我们首先分析一个小程序:

#include<stdio.h>#include<stdlib.h>#include<pthread.h>static int g_count = 0;void* read_write_num(void* _val){    int val = 0;    int i = 0;    for(; i<5000; ++i)    {        val  = g_count;        printf("pthread id is:%x,count is : %d\n",(unsigned long)pthread_self(),g_count);        g_count = val + 1;    }    return NULL;}int main(){    pthread_t tid1;    pthread_t tid2;    pthread_create(&tid1,NULL,read_write_num,NULL);    pthread_create(&tid2,NULL,read_write_num,NULL);    pthread_join(tid1,NULL);    pthread_join(tid2,NULL);    printf("count final val is:%d\n ",g_count);    return 0;}

这里写图片描述

上面程序是创建两个线程,各自都把全局静态变量count增加5000次。正常情况下结果应该是10000,但是程序的最终结果总是5000左右,与正确结果相差很远,这是为什么呢?
这是因为对多线程的程序,发生了访问冲突了。对一个线程要对一个变量进行加1操作,需要进行三步:
1.将数据从内存单元读入寄存器。
2.把寄存器中的变量做加1操作。
3.把加1后的变量重新写回内存单元。

我们来模拟一下上面程序的运行。我们上面的程序有两个线程tid1,tid2。假如此时变量count = 10.
当tid1进行第二步时,从内存读到10,在寄存器加1–>10+1=11。
但是此时tid2切入,从内存读到10,在寄存器加1–>10+1=11。
因为tid1还没有把11写入到内存中,tid2读到的值并不是我们想要的11,这就引发了问题。
解决的方法就是引入互斥锁,获得锁线程可以完成“读–修改–写”的操作,然后释放锁给其他线程,没有获得锁的线程只能等待而不能访问共享数据。这样“读–修改–写”散步操作组成了一个原子操作,要么都执行,要么都不执行。

互斥锁操作函数

互斥锁的数据类型为pthread_mutex_t.
创建互斥锁:

#include <pthread.h>int pthread_mutex_init(pthread_mutex_t* restrict mutex,const pthread_mutexattr_t* restrict attr)

返回值:成功返回0,失败返回错误号。
参数:
pthread_mutex_init()函数是以动态方式创建互斥锁的,参数attr指定了新建互斥锁的属性。如果参数attr为空,则使用默认的互斥锁属性,默认属性为快速互斥锁 。
如果Mutex变量是静态分配的,也可以利用宏定义来初始化。

pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;

此方法相当于pthread_mutex_init初始化并且attr参数为NULL.
销毁互斥锁:

#include <pthread.h>int pthread_mutex_destroy(pthread_mutex_t *mutex);

返回值:成功返回0,失败返回错误码。
加锁:

#include <pthread.h>int pthread_mutex_lock(pthread_mutex_t *mutex);

解锁:

#include <pthread.h>int pthread_mutex_unlock(pthread_mutex_t *mutex);

返回值:成功返回0,失败返回错误号。
一个线程可以调用pthread_mutex_lock还会获得Mutex,若果这时另一个线程已经调用pthread_mutex_lock获得了该Mutex,则当前进程需要挂起等待,直到另一个线程调用pthread_mutex_unlock释Mutex,当前进程被唤醒,才能获得该Mutex并继续执行。
如果一个线程既想获得锁,又不想挂起等待,可以调用pthread_mutex_trylock,如果Mutex已经被另一个线程获得,这个函数会失败返回EBUSY,而不会使线程挂起等待。

#include <pthread.h>int pthread_mutex_trylock(pthread_mutex_t *mutex);

这时我们就可以解决上面那个代码的bug了。

#include<stdio.h>#include<stdlib.h>#include<pthread.h>static int g_count = 0;pthread_mutex_t mutex_lock = PTHREAD_MUTEX_INITIALIZER;void* read_write_num(void* _val){    int val = 0;    int i = 0;    for(; i<5000; ++i)    {        pthread_mutex_lock(&mutex_lock);        val  = g_count;        printf("pthread id is:%x,count is : %d\n",(unsigned long)pthread_self(),g_count);        g_count = val + 1;        pthread_mutex_unlock(&mutex_lock);    }    return NULL;}int main(){    pthread_t tid1;    pthread_t tid2;    pthread_create(&tid1,NULL,read_write_num,NULL);    pthread_create(&tid2,NULL,read_write_num,NULL);    pthread_join(tid1,NULL);    pthread_join(tid2,NULL);    printf("count final val is:%d\n ",g_count);    return 0;}

这里写图片描述

线程死锁

⼀般情况下,如果同⼀个线程先后两次调⽤lock,在第⼆次调⽤时,由于锁已经被占⽤,该线程会挂起等待别的线程释放锁,然⽽锁正是被⾃⼰占⽤着的,该线程又被挂起⽽没有机会释放锁,因此 就永远处于挂起等待状态了,这叫做死锁(Deadlock)。
另一种情况:线程A获 得了锁1,线程B获得了锁2,这时线程A调⽤lock试图获得锁2,结果是需要挂起等待线程B释放 锁2,⽽这时线程B也调⽤lock试图获得锁1,结果是需要挂起等待线程A释放锁1,于是线程A和B都 永远处于挂起状态了。
死锁产生的4个必要条件:
1)互斥条件:指进程对所分配到的资源进行排它性使用,即在一段时间内某资源只由一个进程占用。如果此时还有其它进程请求资源,则请求者只能等待,直至占有资源的进程用毕释放。
2)请求和保持条件:指进程已经保持至少一个资源,但又提出了新的资源请求,而该资源已被其它进程占有,此时请求进程阻塞,但又对自己已获得的其它资源保持不放。
3)不剥夺条件:指进程已获得的资源,在未使用完之前,不能被剥夺,只能在使用完时由自己释放。
4)环路等待条件:指在发生死锁时,必然存在一个进程——资源的环形链,即进程集合{P0,P1,P2,···,Pn}中的P0正在等待一个P1占用的资源;P1正在等待P2占用的资源,……,Pn正在等待已被P0占用的资源。
预防死锁:
只要打破四个必要条件之一就能有效预防死锁的发生:打破互斥条件:改造独占性资源为虚拟资源,大部分资源已无法改造。
打破不可抢占条件:当一进程占有一独占性资源后又申请一独占性资源而无法满足,则退出原占有的资源。
打破占有且申请条件:采用资源预先分配策略,即进程运行前申请全部资源,满足则运行,不然就等待,这样就不会占有且申请。
打破循环等待条件:实现资源有序分配策略,对所有设备实现分类编号,所有进程只能采用按序号递增的形式申请资源。

有序资源分配法


这种算 法资源按某种规则系统中的所有资源统一编号(例如打印机为1、磁带机为2、磁盘为3、等等),申请时必须以上升的次序。系统要求申请进程:
1、对它所必须使用的而且属于同一类的所有资源,必须一次申请完;
2、在申请不同类资源时,必须按各类设备的编号依次申请。例如:进程PA,使用资源的顺序是R1,R2; 进程PB,使用资源的顺序是R2,R1;若采用动态分配有可能形成环路条件,造成死锁。
采用有序资源分配法:R1的编号为1,R2的编号为2;
PA:申请次序应是:R1,R2
PB:申请次序应是:R1,R2
这样就破坏了环路条件,避免了死锁的发生

银行家算法


避免死锁算 法中最有代表性的算 法是Dijkstra E.W 于1968年提出的银行家 算 法:
银行家算法是避免死锁的一种重要方法,防止死锁的机构只能确保上述四个条件之一不出现,则系统就不会发生死锁。通过这个算法可以用来解决生活中的实际问题,如银行贷款等。
程序实现思路银行家算法顾名思义是来源于银行的借贷业务,一定数量的本金要应多个客户的借贷周转,为了防止银行家资金无法周转而倒闭,对每一笔贷款,必须考察其是否能限期归还。在操作 系统中研究资源分配策略时也有类似问题,系统中有限的资源要供多个进程使用,必须保证得到的资源的进程能在有限的时间内归还资源,以供其他进程使用资源。如果资源分配不得到就会发生进程循环等待资源,则进程都无法继续执行下去的死锁现象。
把一个进程需要和已占有资源的情况记录在进程控制中,假定进程控制块PCB其中“状态”有就绪态、等待态和完成态。当进程在处于等待态时,表示系统不能满足该进程当前的资源申请。“资源需求总量”表示进程在整个执行过程中总共要申请的资源量。显然,每个进程的资源需求总量不能超过系统拥有的资源总数, 银行算法进行资源分配可以避免死锁。

原创粉丝点击