讨论避免的方法的时候,先看看什么条件下发生死锁。
这在《现代操作系统》中有描述:
如果一个线程集合产生死锁,那么它是这样的一个线程集合,该集合内的所有线程,都在等待该集合内其它线程释放资源。
按照这种说法,死锁产生的一个必要条件就是至少会形成一个环,该环内每个线程占有一个资源,并请求另一资源。
那么,形成环的一个必要条件就是,加锁的顺序一定不是严格按照顺序加锁的。否则,它就是一个有向无环图,就不可能产生死锁了。
因此,避免死锁的一个方法,就是让那些同时拥有好几把锁的线程加锁顺序一致。这便是书中图11-11中代码所采用的方法。
书中举了两个互斥量的例子。第一个互斥量保护foo类型内的数据结构,而第二个互斥量保护散列表。图11-11代码如下,稍后将简要解释:
#include <stdlib.h>
#include <pthread.h>
#define NHASH 29
#define HASH(id) (((unsigned long)id)%NHASH)
struct foo *fh[NHASH];
pthread_mutex_t hashlock = PTHREAD_MUTEX_INITIALIZER;
struct foo {
int f_count;
pthread_mutex_t f_lock;
int f_id;
struct foo *f_next; /* protected by hashlock */
/* ... more stuff here ... */
};
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);
fp->f_next = fh[idx];
fh[idx] = fp;
pthread_mutex_lock(&fp->f_lock);
pthread_mutex_unlock(&hashlock);
/* ... continue initialization ... */
pthread_mutex_unlock(&fp->f_lock);
}
return(fp);
}
void
foo_hold(struct foo *fp) /* add a reference to the object */
{
pthread_mutex_lock(&fp->f_lock);
fp->f_count++;
pthread_mutex_unlock(&fp->f_lock);
}
struct foo *
foo_find(int id) /* find an existing object */
{
struct foo *fp;
pthread_mutex_lock(&hashlock);
for (fp = fh[HASH(id)]; fp != NULL; fp = fp->f_next) {
if (fp->f_id == id) {
foo_hold(fp);
break;
}
}
pthread_mutex_unlock(&hashlock);
return(fp);
}
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);
free(fp);
} else {
fp->f_count--;
pthread_mutex_unlock(&fp->f_lock);
}
}
只有在foo_alloc函数和foo_rele函数中才同时拥有两把锁,应该使得当同时加两把锁的时候,加锁顺序一定要一致。在foo_alloc函数和foo_find函数中,加锁顺序是先hashlock,后f_lock,因此在foo_rele函数中,同时拥有两把锁的时候,加锁顺序要和foo_alloc相同,这也就是为什么第78行的代码要重新将f_lock解锁,如果不解锁而直接为hashlock加锁,则会在另一个线程调用foo_alloc或者foo_find的时候可能会产生死锁。
课本的图11-12采用和图11-11不同的方法,它只有一个函数同时拥有两把锁,其它函数只有一把锁。这一定不会产生死锁条件。这种方法是一种粗粒度锁的方法。锁的粒度太粗,则将导致过多的线程阻塞,并发性不好。而锁的粒度太细,则增大锁的开销。多线程软件正是在这两者之间折中。