`

分析《进程间通信》一书中的读/写锁策略

阅读更多
以下代码和资料均学习自:《进程间通信》第8章读写锁
其中附件中的代码为自己重新封装后的代码和一个测试代码

编译环境如下
Thread model: posix
gcc version 3.2.2 20030222 (Red Hat Linux 3.2.2-5)

--------------------------------------------------------------------------------
--------------------------------------------------------------------------------
--------------------------------------------------------------------------------
--------------------------------------------------------------------------------
正文:
读优先的读写锁策略

假设对于一个共享的数据区来说,读/写都需要,而写的并发又可能比较频繁一点,如果按照普通的mutex思路可能是以下结构:
lock()

Write()/Read()

Unlock()


对于write来说,对于共享的形式是独占(shared-exclusive);但对于读锁来说对于共享的形式是共享锁(shared lock),即不需要mutex的互斥保护。那么以上的写法中,对于读的情况效率大大的打折。比如一个银行帐户,多个线程可以同时读出某个帐户的收支结余,但是一旦有一个线程需要更新某个给定收支结余,该线程就必须等待所有读出者完成该收支结余的读出,然后只允许该更新线程修改这个收支结余。那么在实现这个策略时,必须遵循以下三个条件
1、 保证并发的读取,不对读取做什么界限的限制,仅仅以一个计数器来表示当前的读取量
2、 有写入线程时,保证写入线程的独立执行
3、 在有写入线程发生,以这个写入线程时间点为界限,之前的等待读线程要执行完,之后的读线程处于等待状态
那么可以设置这么一个结构体`
typedef struct 
{
  pthread_mutex_t	rw_mutex;		/* basic lock on this struct  */
  pthread_cond_t	rw_condreaders;	/* for reader threads waiting */
  pthread_cond_t	rw_condwriters;	/* for writer threads waiting */
  int				rw_magic;	/* for error checking */
  int				rw_nwaitreaders;			/* the number waiting */
  int				rw_nwaitwriters;			/* the number waiting */
  int				rw_refcount;
} pthread_RWlock_t;

/*
*rw_nwaitreaders:当前有读的线程工作时,写的线程阻塞,并且该变量自增
*rw_refcount:表示锁的工作状态,如果是写用-1表示,如果当前无工作状态用0表示,读的
*状态为大于1,其数量代表了当前的读的并发数
*/


再设置两个获取读/写锁函数:
int   pthread_RWlock_rdlock (pthread_RWlock_t *pRw);
int   pthread_RWlock_wrlock (pthread_RWlock_t *pRw);

流程的判断变为如下的工作流程,这里我先把图给出,问题统一放在后面分析
获取读/写锁两个函数大体流程如下:


pthread_RWlock_wrlock函数流程如下:


pthread_RWlock_rdlock函数流程如下:


问题一:先说明一个技巧
这里用了phtread_cond_wait(), 防止一个不断的轮询过程,代码是这样的
	while( pRw->rw_refcount != 0 )
	{
		pRw->rw_nwaitwriters++;

		iResult = pthread_cond_wait(&pRw->rw_condwriters, &pRw->rw_mutex);
		
		pRw->rw_nwaitwriters--;
		if( iResult != 0 )
		{
			break;
		}
	}

注意,这里用了个while.在多线程的情况下,有多个cond_wait进行等待,当rw_mutex的锁被释放时,可能被其他线程抢到(即不保证被本身的线程取到),并对rw_refcount进行修改。如果不对rw_refcount进行判断而直接执行,就可能造成数据的不准确。


问题二:获取读锁时对rw_refcount!= 0的判断
    这要和读锁的rw_refcount <0和rw_nwaitwriters>0两个条件结合起来进行解释。首先假设当前有多个线程进行工作,这时有个写线程进来,发生阻塞,waitwriters自加。这个时候比这个写线程后来的读线程检测到rw_nwaitwriters>0而发生阻塞,在这之前的读线程仍在工作,这里我们还需要知道pthread_RWlock_unlock的部分工作代码

/*
	对读锁的释放为:如果是读锁,refcount减1,信号通知
	对写锁的释放为:如果是写锁,refconut直接置0,信号通知
	*/
	if( pRw->rw_refcount > 0 )
	{
		pRw->rw_refcount--;	
	}
	else if( pRw->rw_refcount == -1 )
	{
		pRw->rw_refcount = 0;	
	}


看吧,把之前对读锁的获取时rw_refcount++和这里的rw_refcount—结合起来,可得知,当读线程都工作完毕时,rw_refcount必然会等于0,这也满足了我们开头提出的三个条件中的最后一条。那么剩下的pthread_RWlock_unlock工作代码如下:

if( pRw->rw_nwaitwriters > 0 )
	{
		/*
		防止发送空的signal
		*/
		if( pRw->rw_refcount == 0 )
		{
			iResult = pthread_cond_signal(&pRw->rw_condwriters);	
		}		
	}	
	else if( pRw->rw_nwaitreaders > 0 )
	{
		iResult = pthread_cond_broadcast(&pRw->rw_condreaders);	
	}


    首先要先判断当前是否有写的线程等待,这里并不是把优先权给写的线程,而是在有大量的并发读线程下,必须给写的线程点机会,否则它永远都执行不了。这里你可能对pRw->rw_refcount == 0有点疑问。作者的原意可能是这样的:一个写线程在工作中,另一个写线程刚好释放完,准备通知另一个正在写的线程,但这个时候>rw_refcount仍等于-1,通知是无意义的。其实我也是,我认为不会触发这个条件。因为写的线程是确保只有一个执行,而在释放锁的时候已经确保pRw->rw_refcount = 0,当然这会在我代码验证后给出。

好了,现在我们以文字的形式整理下所有的思路。
假设有3个读线程进行并发,那么它们一开始没有检测到rw_refcount <0和rw_nwaitwriters>0,于是他们很欢快的取的读的权限进行并发的读取,假设这个读取的时间很长。。。。这个时候一条写线程进入,它判断rw_refcount不等于0,于是进行阻塞。
    在这个时间里,又有3个读线程发生,但是它们检测到了nwaitwriters大于0,也进行阻塞。。终于,最早的3个读线程工作完了,释放完后,通知正在等待的写线程。写线程工作完后,这时候没有其他的写线程进入,所以pthread_cond_broadcast发生了作用,它通知了最后进入的3个读线程。。。


一个大问题,线程被取消掉的死锁
假设有两个线程,读和写,首先保证thread1的先执行
void *thread1(void *)
{
	int iResult;
	
	CRWLock.pthread_RWlock_rdlock(&rwlock);
	cout<<"thread 1 got a read lock"<<endl;
	sleep(3); /*让thread2阻塞在pthread_RWlock_wrlock()*/	
	pthread_cancel(tid2);
	sleep(3);
	iResult = CRWLock.pthread_RWlock_unlock(&rwlock);

	return 0;
}

void *thread2(void *)
{
	cout<<"thread 2 trying to obtain a write lock"<<endl;
	CRWLock.pthread_RWlock_wrlock(&rwlock);
	cout<<"thread2 got a write lock"<<endl;
	sleep(1);
	CRWLock.pthread_RWlock_unlock(&rwlock);
	
	return 0;
}


这段代码的工作模式如下:thread1先获取一个读锁,睡眠3秒,thread2阻塞在获取写锁上,thread1取消掉thread2,最后thread1调用unlock函数。这时候阻塞在了unlock里。
为什么?这得了解一个线程的知识,pthread_cond_wait函数发生时,会自动释放掉一个mutex,以运行其他线程进入等待。(如果没有释放这个mutex,那么就没有什么并发等待的意义了,大家都阻塞在最外面的lock),当thread1进行取消时,这个mutex没有释放(或者看成pthread_cond_wait之前的释放mutex没什么效果)。那么在unlock里,也需要对这个mutex进行访问,而造成了死锁。这也是许多多线程里死锁的隐患。具体的解决方案见代码,其实很简单,就是压入一个释放函数,如果线程发生意外直接返回则调用该函数。

3
2
分享到:
评论

相关推荐

Global site tag (gtag.js) - Google Analytics