这里以文件的引用计数做例子。
什么是引用计数?
即指向这个文件的链接数。 只有当引用计数为0时,才能删除该文件,否则每一次删除仅仅是i节点的引用计数减一。
如果不引入锁, 那么当同时执行两次引用计数相减时,就会出现计数不准确的情况。
一、简单的锁操作
#include "apue.h"
#include <pthread.h>
#include <stdlib.h>
struct foo{
int f_count;
pthread_mutex_t f_lock;
int f_id;
};
struct foo *foo_alloc(int id){
struct foo *fp;
//建立锁时,先malloc清0(免得之前有人乱弄了这块区域的值)
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);
}
}
return (fp);
}
void foo_hold(struct foo *fp){
//加锁, 引用计数+1, 解锁
pthread_mutex_lock(&fp->f_lock);
fp->f_count++;
pthread_mutex_unlock(&fp->f_lock);
}
void foo_rele(struct foo *fp){
pthread_mutex_lock(&fp->f_lock);
if(--fp->f_count == 0){
//先解锁,再释放。
pthread_mutex_unlock(&fp->f_lock);
pthread_mutex_destroy(&fp->f_lock);
}
else
pthread_mutex_unlock(&fp->lock);
}
由于每次只使用到一个资源,暂时不会发生死锁。
但如果每次要请求2个foo, 那么2个进程,请求2个foo时,若顺序不同,就会发生死锁。
二、预防死锁
如果我们的文件i节点根据其文件id,被放在一个哈希链表中。
1.当我们根据i节点指针增加引用计数时, 直接把i节点进行加锁即可。
2.当我们根据文件id进行增加计数时,需要先遍历链表,此时链表需要加锁。找到节点后进行计数增加,此时节点加锁。(注意,这个过程是先 对链表加锁,再对i节点加锁)
3.当我们根据i节点指针,决定删除i节点时, 会先对i节点加锁,然后读取引用计数,发现要被减为0,决定删除,这个删除涉及到链表中的节点删除。
注意!!这时候不可以直接对链表加锁并进行删除, 因为这个步骤和2步骤会发生死锁! 所以此时需要先解i节点锁,在对链表加锁,加完链表锁之后,再去加i节点锁,保证这个加锁顺序。
但是!!正因为这个取消锁的过程,导致中间可能发生了其他变化,故又不得不进行计数判断。。。
可以看出,以上的加锁策略会引发非常大的麻烦,尽量不要出现加锁解锁又加锁的情况,因为解锁到加锁的过程中,会出现其他的问题。
于是程序进行了改进, 第1步和第2步中,都直接对链表加锁(对最大的加锁,以绝后患),不对i节点加锁。
第3步中,也是先对大链表加锁, 找到对应的fp节点后,再进行对应的删除操作。
#include "apue.h"
#include <pthread.h>
#include <stdlib.h>
#define NHASH 29
#define HASH(id) (((unsigned long)id)%NHASH)
struct foo *fh[NHASH];
pthread_mutex_t hashlock = PTHREAD_MUTEX_INITIALIZR;
struct foo{
int f_count;
pthread_mutex_t f_lock;
int f_id;
struct foo *f_next;
};
struct foo *foo_alloc(int id){
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); //用key取余法取哈希
pthread_mutex_lock(&hashlock);
fp->f_next = fh[idx]; //哈希链表的头插法,插在idx对应的队列上。
fh[idx] = fp;
pthread_mutex_lock(&fp->f_lock);
//为什么要先上fp锁再解hash锁? 为了防止刚解完哈希锁,有人插队。
pthread_mutex_unlock(&hashlock);
/******执行fp的初始化操作****/
pthread_mutex_unlock(&fp->f_lock);
}
return (fp);
}
void foo_hold(struct foo *fp){
//加锁, 引用计数+1, 解锁
//注意,这里是用hashlock锁
pthread_mutex_lock(&hashlock);
fp->f_count++;
pthread_mutex_unlock(&hashlock);
}
struct foo *foo_find(int id){
struct foo *fp;
pthread_mutex_lock(&hashlock);
//从id对应的哈希链表中,找到id相等的那个点,引用数+1,返回
for( fp = fh[HASH(id)]; fp != NULL ; fp = fp->f_next){
if(fp->f_id == id){
fp->f_count++;
break;
}
}
pthread_mutex_unlock(&hashlock);
return fp;
}
void foo_rele(struct foo *fp){
pthread_mutex_lock(&hashlock);
if(--fp->f_count == 0){
idx = HASH(fp->f_id);
tfp = fh[idx];
if(tfp == fp){
fh[idx] = fp->f_next;
}else {
//找到那个节点,在链表上删除
while (tfp->f_next != fp)
tfp = tfp->fnext;
tfp->f_next = fp->f_next;
}
pthread_mutex_unlock(&hashlock);
pthread_mutex_destroy(&fp->f_lock);
free(fp);
}
else
pthread_mutex_unlock(&hashlock);
}
从上面的改进中可以看出, 加锁不能随便加,例如不能因为我修改的是i节点,就直接对i节点加锁,而不考虑全局或死锁的问题。改进中利用了哈希链表最大的特点, 增删操作时,都先直接先锁表, 减少了不必要的死锁处理。
有时候互斥量是不必要的, 例如读一个链表里的信息时, 没有必要锁链表,直接查找并返回即可,不会有冲突,
但是要修改链表时,必须要锁住。
这时候引入了读写锁。
当只有读锁或者没有锁时,可以继续加锁读,锁数量+1
当没有锁时,可以加锁写。
#include <stdlib.h>
#include <pthread.h>
//线程
//储存了线程结构体j_id,以及一个双向指针
struct job{
struct job *j_next;
struct job *j_prev;
pthread_t j_id;
};
//线程双向链式队列
//只能在队头和队尾进行插入,但可以在任意位置删除。
struct queue{
struct job *q_head;
struct job *q_tail;
pthread_rwlock_t q_lock;
};
int queue_init(struct queue *qp){
int err;
qp->q_head = NULL;
qp->q_tail = NULL;
err = pthread_rwlock_init(&qp->q_lock, NULL);
if( err != 0)
return (err);
/*初始化……*/
return 0;
}
void job_insert(struct queue *qp, struct job *jp){
//上写锁
pthread_rwlock_wrlock(&qp->q_lock);
//jp是要插入的作业。
jp->j_next = qp->q_head;
jp->j_prev = NULL;
//修改作业头的前连接
//注意,双向链表队列插入时,一定要小心空的情况
if(qp->q_head != NULL)
qp->q_head->j_prev = jp;
else
qp->q_tail = jp;
qp->q_head = jp;
pthread_rwlock_unlock(&qp->q_lock);
}
void job_remove(struct queue *qp, struct job *jp){
pthread_rwlock_wrlock(&qp->q_lock);
//一定要区分所删点是头节点或者尾节点
//并处理删除后的前后的关系连接
if(jp == qp->q_head){
qp->q_head = jp->j_next;
if(jp == qp->tail){
qp->tail = NULL;
}
else
jp->j_next->j_prev = jp->j_prev;
}else if(jp == qp->q_tail){
qp->q_tail = jp->j_prev;
if(jp == qp->q_head)
q_head = NULL;
else jp->j_prev->j_next = jp->j_next;
}
else{
jp->j_prev->j_next = jp->j_next;
jp->j_next->j_prev = jp->j_prev;
}
pthread_rwlock_unlock(&qp->q_lock);
}
//查找当前队列中对应id的线程,并返回
struct job *job_find(struct queue *qp, pthread_t id){
struct job *jp;
//读锁可能会返回不成功,因为可能被人写了
//但是写锁不需要这一个返回NULL,因为写是会阻塞的。
if(pthread_rwlock_rdlock(&qp->q_lock) != 0)
return NULL;
for(jp = qp->q_head; jp != NULL; jp = jp->j_next)
if(pthread_equal(jp->j_id, id))
break;
pthread_rwlock_unlock(&qp->q_lock);
return jp;
}
显然执行时, 每当create一个线程,就给队列加一个,把线程的信息放入。
然后经常会需要查找线程,这时候用读锁即可,提高效率。