APUE读书笔记——线程互斥(互斥量、读写锁)

这里以文件的引用计数做例子。

什么是引用计数?

    即指向这个文件的链接数。 只有当引用计数为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一个线程,就给队列加一个,把线程的信息放入。

然后经常会需要查找线程,这时候用读锁即可,提高效率。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值