`

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

阅读更多
以下代码和资料均学习自:《进程间通信》第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
分享到:
评论

相关推荐

    《ORANGE’S:一个操作系统的实现》读书笔记(十六)进程(四)文章代码

    通信是进程间交换信息的方式,分为共享内存和消息传递两种主要形式。在ORANGE’S中,可能会看到如管道、消息队列、信号量等通信机制的实现。共享内存允许进程直接读写同一块内存区域,而消息传递则是通过发送和接收...

    读者写者问题,操作系统课程设计报告书.doc

    在操作系统中,读者写者问题是描述多个进程对共享资源访问的一种情况。读者可以同时读取数据,而写者必须独占资源来修改数据,以防止写者写入的数据被多个读者同时读取导致数据不一致。课程设计的目标是实现一个能够...

    MulticoreApplicationProgramming读书笔记.zip

    MPI程序通过进程间的通信进行数据交换,实现并行计算。 3. CUDA:NVIDIA公司推出的GPU并行计算框架,专为CUDA兼容的GPU设计,提供了一种直接访问GPU硬件资源的编程方式,适合于高性能计算和图形处理。 四、并行编程...

    Linux内核源代码情景分析 (上下册 高清非扫描 )

    - 进程间通信(IPC)是多个进程之间交换数据和同步的方法。 - Unix提供了一系列IPC机制,如管道、信号等。 - **6.2 管道和系统调用pipe()** - 管道是一种简单的IPC机制,用于在两个进程之间传递数据。 - `pipe`...

    一本经典的多线程书籍 Java并发编程 设计原则与模式 第二版 (英文原版)

    6. **线程通信**:讲述了wait()、notify()和notifyAll()方法以及它们在实现线程间通信中的角色。同时,还讨论了高级的阻塞队列(BlockingQueue)及其在生产者-消费者模型中的应用。 7. **并发设计模式**:介绍了...

    [中文]Java并发编程的艺术

    8. **并发设计模式**:书中还会介绍一些经典的并发设计模式,如生产者消费者模型、读写锁策略等,这些模式在实际开发中有着广泛的应用。 9. **并发编程最佳实践**:除了理论知识,书中还会分享一些并发编程的最佳...

    Linux Kernel核心中文手册

    在Linux内核中,进程管理是核心功能之一。它涉及进程的创建、调度、同步和通信等过程。手册可能会详细介绍`fork()`、`execve()`、`waitpid()`等系统调用,以及进程状态的转换,如运行(Runnable)、睡眠(Sleeping)...

    Introduction to concurrency theory

    - **读者-写者问题**:介绍如何在多读少写场景下设计高效的并发控制策略。 #### 六、总结 《Introduction to Concurrency Theory》是一本全面介绍并发理论的教材,不仅涵盖了基本的概念和技术,还涉及到了过程代数...

    UNIX 高级教程系统技术内幕

    6.3 System V 的进程间通信 6.3.1 公共元素 6.3.2 信号量 6.3.3 消息队列 6.3.4 共享内存 6.3.5 讨论 6.4 Mach IPC 6.4.1 基本概念 6.5 消息 6.5.1 消息的数据结构 6.5.2 消息传递接口 6.6 端口 6.6.1 端口名字空间 ...

    UNIX环境高级编程.pdf

    - 进程间通信(IPC) - 信号处理机制 2. **线程控制** - 线程的基本概念 - 线程同步机制 - 多线程程序设计技巧 3. **高级文件I/O** - 文件描述符操作 - 高级文件访问方法 - 文件锁定与共享内存 4. **网络...

    Pthread-Primer.rar_pthread_unix primer

    在pthread库中,主要使用条件变量和互斥锁实现线程间的通信和同步。 六、线程属性 pthread库允许设置线程的属性,如调度策略、栈大小等。`pthread_attr_init()`和`pthread_attr_set*()`函数可以用来初始化和修改...

    java thread的教程

    - **CopyOnWriteArrayList**:写时复制策略,适用于读多写少的场景。 - **ConcurrentHashMap**:线程安全的哈希表实现。 - **ConcurrentLinkedQueue**:基于链表的无界队列。 **线程局部变量:** - **ThreadLocal类...

    java并发编程实战

    - `CopyOnWriteArrayList`和`CopyOnWriteArraySet`:讨论了这些线程安全的集合类,适合于读多写少的场景。 4. **原子变量和volatile** - `AtomicInteger`等原子类:阐述了如何使用原子变量进行无锁编程,保证数据...

    POSIX多线程程序设计中文版

    - **读者-写者问题**:解决多个读线程和一个写线程访问共享资源的问题。 - **屏障同步**:使多个线程同步到达某个点后再继续执行。 #### 五、案例分析与实践 **1. 实例解析** - 通过具体的实例来展示如何使用...

    V7.62 ETest产品介绍.pptx

    - **通道读/写**:读取或写入数据到特定的通信通道。 - **协议读/写/解码/编码**:操作复杂的协议数据单元。 **2. 内容附件** - **二进制文件/文本文件**:读写不同类型的文件。 **3. 自动化测试** - **断言操作...

    C#微软培训资料

    17.3 读 写 文 件 .222 17.4 异步文件操作 .227 17.5 小 结 .234 第十八章 高 级 话 题 .235 18.1 注册表编程 .235 18.2 在 C #代码中调用 C++和 VB 编写的组件 .240 18.3 版 本 控 制 .249 18.4 代 ...

Global site tag (gtag.js) - Google Analytics