1 什么是银行家算法
1.1 死锁的定义
死锁是由信号量引入的一种运行时错误。 在一组进程发生死锁的情况下,这组死锁线程中的每一个线程,都在等待另一个进程所占有的资源,此时线程被阻塞了,等待一个永远不为真的条件。
1.2 银行家算法
银行家算法是最具代表性的避免死锁的算法。在银行中,客户申请贷款的数量是有限的,每个客户在第一次申请贷款时要声明完成该项目所需的最大资金量,在满足所有贷款要求时,客户应及时归还。银行家在客户申请的贷款数量不超过自己拥有的最大值时,都应尽量满足客户的需要。在这样的描述中,银行家就好比操作系统,资金就是资源,客户就相当于要申请资源的 进程。
它的流程如下。在这里面最关键的就是安全状态的判定,当有进程向系统申请资源时,系统不会直接把资源分配给进程。而是先做一个安全判定,假若分配后系统仍处于安全状态,则允许分配。那什么是系统安全状态呢?
1.3 系统安全状态
1.3.1 定义
系统安全状态是指系统能够按某种推进顺序(p1、p2...pn)为每个进程pi分配其所需资源,直至每个进程对资源的最大需求。而系统不安全状态是无法进行推进,使得每个进程pi都能够获得其所需的最大需求。 那么关键就是如何推进。
1.3.2 进程的推进
其实进程的推进,是对系统资源分配的一种预判或模拟,它需要对所有进程的资源请求进行模拟。原理如下图。
因为只是一种模拟,其中改变了系统可用资源的数量,所以之后要对系统可用资源进行复原。如果推进失败,要将已经分配给申请进程的资源合并回系统可用资源,表示申请失败。
2 设计分析
2.1 系统对象及其交互
进程通过requestResource方法向系统申请资源,申请的资源数量requestNeed在1和maxNeed-ownNeed之间。当已申请的资源数量ownNeed等于maxNeed时,进程对资源利用完成后,可对申请的所有资源进行释放。
2.2 需要注意的问题
2.2.1 多线程环境需要注意的问题
- 需要保证多线程中,系统分配资源attainResource和释放资源freeResource调用的原子性,可用互斥量解决;问题也可扩大为对共享的系统对象的互斥访问。
- 控制台输出时,需要避免线程之间的多次争夺导致输出乱序,尽量使用一个完整语句一次性输出。例如:在C++中,cout << A << B语句分为了两次调用,可能在输出A后,线程发生切换,导致A和B输出不连续。
- 多线程随机数问题,可用线程ID作为随机数种子
2.2.2 其他注意问题
- 银行家算法进程推进时,在判定过程中,找到符合条件的进程后要再次开始从进程列表的第一个开始找起直到遇到了进程列表的最后一个进程,保证每次查找每个进程都有机会获得资源。
- 进程列表中进程添加和删除时机,进程申请成功时无重复地添加,进程释放资源时从进程列表中删除该进程记录
- 资源的改变时机,包括系统可用资源的改变和进程已申请资源的改变
- 方法参数的设置判断
- 银行家算法的逻辑
3 核心代码分析
3.1 进程核心代码分析
1 bool Process::requestResource(unsigned int num, System &s) 2 { 3 bool rtn = false; 4 int res = 0; 5 if (!setRequestNeed(num)) 6 return false; 7 res = s.attainResource(this); 8 9 if(res == 0)10 rtn = true;11 else12 rtn = false; 13 return rtn;14 }
进程的申请资源函数,主要是对自身属性requestNeed进行设置,同时将自身指针传递给系统,告知系统分配资源。
将进程对象传递给系统对象的好处:
- 系统对象可以方便得知进程对象的最大需求、请求资源数量以及其他进程信息,方便系统银行家算法的进行,维护进程列表
- 可以减少方法的参数数量
- 减少结果输出的复杂性
3.2 系统核心代码
3.2.1 银行家算法主要逻辑代码
1 unsigned int System::attainResource(Process *process) 2 { 3 //避免多个线程同时操作 4 wait(); 5 int rtn = 0; 6 unsigned int requestNum = process->getRequestNeed(); 7 unsigned int available_bk = getAvailable(); 8 unsigned int ownNeed_bk = process->getOwnNeed(); 9 if(requestNum <= getAvailable())10 {11 //假设允许该申请,系统资源减少,进程占有资源增多;失败时要进行恢复12 setAvailable(getAvailable() - requestNum);13 process->setOwnNeed(process->getOwnNeed() + requestNum);14 //进行系统安全状态的判断15 if(banker(process))16 {17 //成功,则将该进程添加入进程列表18 processList.insert(process);19 rtn = 0;20 }21 else22 {23 //失败,要将数据复原24 setAvailable(available_bk);25 process->setOwnNeed(ownNeed_bk);26 rtn = 1;27 }28 //安全序列(包括了不安全序列)输出29 printProcessList(std::cout);30 }31 else32 {33 rtn = 1;34 }35 release();36 return rtn;37 }
上面的代码是银行家算法的主要逻辑,如下图。
设计时,为了防止多个线程对共享的系统对象同时操作,需要在函数头加上wait和函数尾加上release函数,实现互斥。
3.2.2 银行家算法的安全状态代码分析
1 bool System::banker(Process *process) 2 { 3 bool rtn; 4 unsigned int available_bk = getAvailable(); 5 unsigned int num = process->getRequestNeed(); 6 processUid.clear(); 7 for(auto i = processList.begin(); i != processList.end(); ) 8 { 9 if(false == (*i)->isFinish() && (*i)->getMaxNeed() - (*i)->getOwnNeed() <= getAvailable())10 {11 (*i)->setFinish(true);12 setAvailable(getAvailable()+(*i)->getOwnNeed());13 processUid.push_back((*i)->getUid());14 i = processList.begin();15 }16 else17 {18 ++i;19 }20 }21 rtn = true;22 for(auto i = processList.begin(); i != processList.end(); ++i)23 {24 if((*i)->isFinish() == false)25 rtn = false;26 (*i)->setFinish(false);27 }28 setAvailable(available_bk);29 return rtn;30 }
这个算法的核心就在于每一次都会在进程列表中找出一个符合条件(见代码)的进程,设为true并且将其所拥有的资源合并到系统可用资源,增加系统可用资源;同时再次在进程列表中查找直到都不符合条件为止。不符合条件中,如果所有进程的finish都为true则代表分配后系统仍处于安全状态。
注意:在查找前要保证进程列表中的进程的finish都为false。
4 Github代码
-
https://github.com/cposture/Banker