apue学习第十八天——从哲学家就餐问题看死锁

搞了几天坑爹的论文整理和专利,这下终于都搞完了,可以继续好好看书啦!

前几天看了线程基础以及5种基本的同步机制。那好,对于死锁这个快听腻了的词,先实现一个哲学家问题上的死锁,然后看怎样去解决它。


问题描述:也就是一个桌上围五个人,每个人左右手边各有一根筷子(不用管原著中的刀叉啦),先各思考一会(假设思考rand()%10时间吧),然后先拿左手筷子,再拿右手筷子;吃饭(假设吃rand()%10时间);吃完之后,先放左手筷子,在放下右手筷子;大家都吃完之后,一起说:yummy!(汗~~)

这是Dijkstra提出的经典死锁场景(想到Dijkstra算法了吧,解决加权有向图的单源最短路径问题的~那么,贪心的prim呢?负权值的Bellman-Ford呢?Prime和Dijkstra算法中的具体区别?有负权值的任意两点间的最短路径中是不是也用了Dijkstra?这几个图论算法没懵吧?哈哈),扯的太远了,我把我自己写的代码贴上来:

#include <stdio.h>
#include <pthread.h>
#include <stdlib.h>

pthread_mutex_t chop_locks[5] = {PTHREAD_MUTEX_INITIALIZER, PTHREAD_MUTEX_INITIALIZER, PTHREAD_MUTEX_INITIALIZER, PTHREAD_MUTEX_INITIALIZER, PTHREAD_MUTEX_INITIALIZER};

struct phil
{
	pthread_t tid;
	char phil_name;
	int left_chop;
	int right_chop;
};

void *simulate(void *arg)
{
	struct phil *ph = arg;

	int left_index = ph->left_chop - 1;
	int right_index = ph->right_chop - 1;

	/* think time */
	int think_time = rand()%10;
	printf("Philosopher %c thinks %d seconds.\n", ph->phil_name, think_time);
	usleep(think_time);

	/* fetches the left chopstick */
	pthread_mutex_lock(&chop_locks[left_index]);
	printf("Philosopher %c fetches chopstick %d\n", ph->phil_name, ph->left_chop);

	/* fetches the right chopstick */
	pthread_mutex_lock(&chop_locks[right_index]);
	printf("Philosopher %c fetches chopstick %d\n", ph->phil_name, ph->right_chop);

	/* eating */
	int eat_time = rand()%10;
	printf("Philosopher %c eats %d seconds.\n", ph->phil_name, eat_time);
	usleep(eat_time);

	/*release chopsticks*/
	pthread_mutex_unlock(&chop_locks[left_index]);
	pthread_mutex_unlock(&chop_locks[right_index]);
	printf("Philosopher %c releases chopsticks %d %d\n", ph->phil_name, ph->left_chop, ph->right_chop);

	return((void *)0);

}

int main(void)
{
	//struct phil *phils[5];
	struct phil *phils;
	
	int err;
	int i;
	int phil_num = 5;
	/* Initialize all philosophers */
	for(i=0; i<phil_num; i++)
	{
		phils = calloc(phil_num, sizeof(struct phil));
	}

	phils[0].phil_name = 'A';
	phils[0].left_chop = 5;
	phils[0].right_chop = 1;

	phils[1].phil_name = 'B';
	phils[1].left_chop = 1;
	phils[1].right_chop = 2;

	phils[2].phil_name = 'C';
	phils[2].left_chop = 2;
	phils[2].right_chop = 3;

	phils[3].phil_name = 'D';
	phils[3].left_chop = 3;
	phils[3].right_chop = 4;

	phils[4].phil_name = 'E';
	phils[4].left_chop = 4;
	phils[4].right_chop = 5;

	/* craete a thread for each philosopher */	
	for(i=0; i<phil_num; i++)
	{
		err = pthread_create(&phils[i].tid, NULL, simulate, &phils[i]);
		if(err != 0)
		{
			printf("create error!\n");
			exit(err);
		}
	}

	/* wait all threads join */
	for(i=0; i<phil_num; i++)
	{
		pthread_join(phils[i].tid, NULL);
	}

	/* If all philosophers finish eating, print this sentence. */
	printf("We all have enjoyed our dinner!\n");

	free(phils);
	exit(0);
}

好吧,这里面一个筷子代表一个mutex,不过chop_locks[5]的初始化的确有点丑。写代码时这个对我的帮助很大(函数不会用的话就查这个吧,示例很棒):http://man7.org/linux/man-pages/man3/pthread_create.3.html。那我就把写这个程序时注意的几点列出来:

(1)是关于mutex的初始化,注意,MUTEX_INITIALIZER只能用于静态分配的互斥量,能不能在函数外面声明在里面赋值呢?绝对不行,我试过,不信你试试看?另外,如果想在后面初始化mutex的话,乖乖用pthread_mutex_init吧!

(2)看main函数中对struct phil *phils的定义,注意啊,是后面的calloc,因为有5个长度为sizeof(struct phil)的结构体,所以分配长度用phils = calloc(5, sizeof(struct phil)); 不分配能用吗?绝对不能,不信你也试试。比如说你要声明一个结构体指针想用它的话,只有struct A *a = malloc(sizeof(struct A)); 那你说你想直接struct A *a;然后把其它的结构体直接指向a?没门!为什么?好,那我们来看为什么:

struct A *a的确分配了空间,但它是给a分配了一个A*的存储空间,即a中存放的是指向struct A*的地址;但是,并不存在一个真实的struct A的存储空间。所以说,仅仅声明完struct A *a; 当然不可以用a->phil_name,原因是空间都没有,哪里有phil_name这个东西啊?!但是,如果事先struct A b;然后a = &b; 当然是对的啦!(还有关于typedef的,这个可以去掉每次定义前的struct,仔细想想看?)

(3)下一点强调的就是pthread_create这个线程创建的函数了,看定义吧:

int pthread_create(pthread_t *restrict tidp, const pthread_attr_t *restrict attr, void *(*start_rtn)(void *), void *restrict arg);
挺长的哈,程序中的具体实现为:

pthread_create(&phils[i].tid, NULL, simulate, &phils[i]);
好,那对应起来看,第一个是thread id,不多说;第二是线程属性;第三个是该线程的处理函数地址,返回void *类型,注意;第四个是传入处理函数的参数,因为参数只有一个,所以传入的参数必须放到一个结构中,这里也需注意,看simulate中如何处理传入的参数。

(4)第一次编写完毕后编译成功但运行有问题:Segmentation fault (core dumped)错误。这是个啥错呢?一般来说是指针指到程序段外去了。具体调试的时候可以用生成的core文件调试,这个我没有尝试,不过看这里,比较详细:http://www.cnblogs.com/panfeng412/archive/2011/11/06/segmentation-fault-in-linux.html

(5)在思考和吃的时候,用sleep()太慢啦,所以程序中该用usleep()函数,沉睡microsecond。


好,要注意的细节就是以上几点,gcc之后运行程序,你可以发现有时会产生死锁,有时可以运行成功,接下来我们就看产生死锁的条件有哪些,当然,这些本科时候的操作系统已经讲得烂透啦!


死锁是1965年Dijkstra(又是他!)研究银行家算法时提出的,这是个在并发程序设计中的一大难题啊。什么是死锁?我理解的就是一种僵持状态,那么众所周知,当系统同时满足以下4个conditions的时候就可能产生死锁(不要给我说神马4个必要条件,我从高中上到现在都没弄明白啥是充分条件,啥是必要条件,况且这对我数学也没啥影响):

(1)mutual exclusion 互斥; (2)hold and wait 占有且等待; (3)no preemption 不可抢占; (4)circular wait 循环等待。

四个条件很直接,想一想记一记。回忆一下当时我们用什么来分析死锁的来着?对啦,是资源分配图(Resource Allocation Graph),这个就不多说了,怎么画自己查吧~


deadlock资料:【Deadlock(死結)】http://www.csie.ntnu.edu.tw/~swanky/os/chap5.htm;

【进程死锁及解决办法】http://blog.csdn.net/abigale1011/article/details/6450845;

【Wikipedia Deadlock】http://en.wikipedia.org/wiki/Deadlock


那么,有什么办法可以不让死锁影响我们的系统呢?

1. ignore; 2. prevention; 3. avoidance(Banker's algorithm(contain Safety algorithm)); 4. detection(alogithm) & recovery。

好,那我们一个个来分析:

1. ignore

汗,这个ignore还真算是一种策略。不过想想,在企业大型系统工程中,为了不影响系统性能,不多花心力,有时候百万分之一发生的错误还真有可能ignore掉。

2. prevention

它的观点是:打破死锁四个条件中的任意一个,可确保死锁永不发生。那我们就来分析打破四个条件。

(1)打破互斥。可以吗?显然不行,互斥怎么可能被打破!那征用资源岂不是乱套了!

(2)打破占有且等待。这就要保证,进程不需要等待,用资源的时候一次性都拿过来。既然预先要把所有要用的资源都拿过来,就需要资源预先分配的策略了。但是资源预先分配的策略真的好吗?

  • 进程是动态的,不可预知它要用到的所有资源;
  • 资源利用率显然极低,因为某个资源即使只用一次的话,也会被该进程在整个生命周期中一直占用;
  • 进程并发性低。因为资源有限,必然影响并发进程数量。

所以,占有且等待也不行。

(3)打破不可强占。这个可行吗?可行倒是可行,但是,这要保证申请新资源不被满足时,释放手头所有资源。实现起来困难,而且会降低系统效率。

(4)打破循环等待。只需要按一定顺序获取资源,不让它形成环路就可以了。常用方法是给所有资源从大到小编号,用的时候只能从大到小申请。但是,你不觉得这有点太那啥了么?必须编号所有资源并且还必须严格遵循次序,这些都要系统开销啊!

Summary:优点是决不会有deadlock出现;缺点是资源利用率down~系统性能down~

3.avoidance

当process request resource时,系统考虑

a. 目前可用的资源数量 b. 各process对资源的需求量 c. 各process目前持有的资源量

来判断此次request是否会导致unsafe state,是的话就分配,不是的话就使其过段时间再申请。

具体涉及Banker's Algorithm(内含Safety Algorithm)。具体的就不说啦,看上述网页吧!

4. detection(algorithm) & recovery

如果不用deadlock prevention和avoidance,那么就需要detection啦!它当然是用来检测死锁,如果死锁存在,那么就打破它!

所以,实际系统中,由于死锁预防与死锁避免实现困难并有额外系统开销,所以很多使用死锁检测与恢复的方法。

(1)死锁检测算法(deadlock detection algorithm)

也就是通过占有矩阵、申请矩阵神马的算啦,具体做法看上面网站,一般是每隔一段时间检测或者CPU利用率降低到一定程度检测。

(2)恢复

a. 重启(呵呵,呵呵) b. 终止进程,回收资源(可能全部回收,也可能回收代价最小的) c. 回退策略(听起来很好,但有时候记录每一步的状态着实不容易)。


好啦,以上就是死锁以及死锁的解决办法。看完了理论,我们看书里的11.6.2,主要是通过指定加锁顺序来避免死锁的(当然,我认为在我们菜鸟们写代码的时候,这是最简单的一种策略,比如上面的哲学家吃饭问题~)。看代码11-11和11-12的对比吧!

代码分析:程序中存在两个mutex,hashlock用于保护hash table的变化,f_lock用于保护struct foo中数值的变化。在函数void foo_rele中,11-11是以相同顺序加锁以避免死锁的例子;11-12中弃用两个mutex,是单以一个hashlock保护所有值的例子。 很容易发现,11-11代码复杂是细粒度的,11-12代码简单是粗粒度的;对比的目的是提示我们平衡锁的粒度,在代码复杂性和性能之间找到平衡。

所有代码在书上,我就不全部贴出来了,这里只贴出我认为应该注意的3个函数:a.结构体分配foo_alloc函数  b.粗粒度的foo_rele函数  c. 细粒度的foo_rele函数。具体的我们看代码:

#include <stdlib.h>
#include <pthread.h>

#define NHASH 29
#define HASH(id) (((unsigned long)id)%NHASH)

struct foo *fh[NHASH];	//存放的是struct foo*
pthread_mutex_t hashlock = PTHREAD_MUTEX_INITIALIZER;	//hashlock锁

struct foo {
int f_count;
pthread_mutex_t f_lock;	//f_lock锁
int f_id;
struct foo *f_next; /* protected by hashlock */
/* ... more stuff here ... */
};

/* a. 结构体分配函数,注意是hash table的链式存储*/
struct foo *
foo_alloc(int id) /* allocate the object */
{
	struct foo *fp;
	int idx;
	if ((fp = malloc(sizeof(struct foo))) != NULL) {	//看这儿的分配,记起什么了吗?
		fp->f_count = 1;
		fp->f_id = id;
		if (pthread_mutex_init(&fp->f_lock, NULL) != 0) {
			free(fp);
			return(NULL);
		}
		idx = HASH(id);
		
		pthread_mutex_lock(&hashlock);	//注意这两句话,链式hash table,新加的struct查到链的开头而不是结尾;
		fp->f_next = fh[idx];
		
		fh[idx] = fp;
		pthread_mutex_lock(&fp->f_lock);	//在hashlock释放之前加锁,保证是刚分配的那块struct
		pthread_mutex_unlock(&hashlock);
		/* ... continue initialization ... */
		pthread_mutex_unlock(&fp->f_lock);
	}
	return(fp);
}

/* b. 11-11中,用两个互斥量保护,细粒度的加锁过程*/
void
foo_rele(struct foo *fp) /* release a reference to the object */
{
	struct foo *tfp;
	int idx;
	pthread_mutex_lock(&fp->f_lock);
	
	if (fp->f_count == 1) { /* last reference */
		pthread_mutex_unlock(&fp->f_lock);	//下面三句是保证顺序加锁;
		pthread_mutex_lock(&hashlock);
		pthread_mutex_lock(&fp->f_lock);
		/* need to recheck the condition */
		if (fp->f_count != 1) {					//想想此处为什么重新检测,想不通的话再罚写一遍程序!
			fp->f_count--;
			pthread_mutex_unlock(&fp->f_lock);
			pthread_mutex_unlock(&hashlock);
			return;
		}
	/* remove from list */
	idx = HASH(fp->f_id);				//后面都是指针常用的操作,不过很容易出错;
	tfp = fh[idx];
	if (tfp == fp) {
		fh[idx] = fp->f_next;
	} else {
	while (tfp->f_next != fp)
		tfp = tfp->f_next;
		tfp->f_next = fp->f_next;
	}
	pthread_mutex_unlock(&hashlock);
	pthread_mutex_unlock(&fp->f_lock);
	pthread_mutex_destroy(&fp->f_lock);	//用完了destroy掉
	free(fp);
	} else {
		fp->f_count--;
		pthread_mutex_unlock(&fp->f_lock);
	}
}

/* c. 11-12中,只用hashlock保护,粗粒度的加锁过程*/
void
foo_rele(struct foo *fp) /* release a reference to the object */
{
	struct foo *tfp;
	int idx;
	pthread_mutex_lock(&hashlock);
	if (--fp->f_count == 0) { /* last reference, remove from list */
		idx = HASH(fp->f_id);
		tfp = fh[idx];
		if (tfp == fp) {
			fh[idx] = fp->f_next;
		} else {
			while (tfp->f_next != fp)
			tfp = tfp->f_next;
			tfp->f_next = fp->f_next;
		}
		pthread_mutex_unlock(&hashlock);
		pthread_mutex_destroy(&fp->f_lock);
		free(fp);
	} else {
		pthread_mutex_unlock(&hashlock);
	}
}

很长很长,好啦,那就来个突兀的结束吧,这就是死锁以及死锁的处理方式!


你可能感兴趣的:(apue学习第十八天——从哲学家就餐问题看死锁)