读者-写者问题精析——分析与优化

  最近遇到了进程并发与互斥相关的问题。其中一个经典的问题就是读者-写者问题。本文对读者-写者问题的经典模式——读者优先,与两个拓展模式——写者优先、读写公平进行了描述和分析。笔者通过总结归纳,对互斥锁的理念(Idea)有了进一步的了解,并基于此提出了一些可能的方法。
(本文描述中,锁和信号量两个概念混杂使用,指代同一对象)

问题描述

读者-写者(Reader-Writer)问题,描述了多个读者与写者对同一共享资源(如内存,文件等)并发访问的情境。其主要特点为:

  1. 读者间不互斥:读取操作不会改变资源内容,故其行为不具有互斥性。表现为:当有进程正在读取内容时,其他进程仍可以并发地执行读取操作
  2. 读写/写写互斥:由于写操作会改变资源内容,写操作与其他任何操作都互斥。表现为:当一个进程正在写时,其他进程均阻塞等待
  3. 多个读写者:本条件在读者优先、读写公平中容易被忽略(笔者一开始就忽略了这个条件),在写者优先中有体现,但其存在性在任何模式下都不可忽略。该特性将在下面的模式分析中提及。

以上是读者-写者问题的共同特点,下面对各个模式进行分析。


模式分析

读者优先

读者优先模式是出现于教科书中最多的模式。读者优先级高于写者。

特点

当有进程正在读取内容时,读者可以不受阻塞地直接读取内容。写者需要等待所有读取请求完成后才开始写入。

实现

设置信号量:

  • 互斥信号量wm(write mutex)写写/读写互斥锁, rcm(read count mutex)计数器锁,ws(writer single)限制读者等待队列,见下;
  • 计数器rc (read count),表示当前在读取的读者数。

实现代码如下:(C表述,第一次实现包含了信号量结构体定义等内容,下同)

typedef struct {
	int value;
	void *list;	// list of process struct, waiting for semaphore
}semphr;

# define mutex semphr
# define counter int
# define NULL 0

mutex rcm = {1, NULL}, wm = {1, NULL}, ws = {1, NULL};
counter rc = 0;

void P(semphr *sem);
void V(semphr *sem);

void reader()
{
	P(&rcm);		// acquire read counter lock
	if (rc == 0)	// the first reader?
		P(&wm);		// acquire writer mutex, wait for write progress done
	rc++;
	V(&rcm);		// release counter lock
	// Do-read;
	P(&rcm);		// acquire again
	rc--;
	if (rc == 0)	// the last reader?
		V(&wm);		// release writer mutex, writer can acquire it
	V(&rcm);
}

void writer()
{
	P(&ws);			// there must be SINGLE writer waiting/having wm lock
	P(&wm);			// acquire for writer mutex
	// Do-Write;
	V(&wm);
	V(&ws);
}
  • 具体说明都在注释中,简单实现不是本文的目的,重点请看下节“分析要点”
分析要点
  • 此模式下,由于写操作的互斥性,写者都需要等待获取wm锁,而一旦中间由读者持有,其他读者都可以跳过等待的写者,直接开始读写。这个性质要求同时只能有一个写者请求或持有wm
    若不加ws,则会引发顺序错误,图例:
    rw

    • 为此,添加一个writer single互斥锁,仅允许一个writer等待/持有writer mutex,这样当一个writer持有wm时(正在写入),Reader会优先于Writer排队等待wm,达到读者优先目的。
  • 由于写操作的互斥性,以及写者优先级位于最低(低于读者),写者不需要计数器记录等待数,只需加入ws信号量请求队列即可。

读写公平

读者优先模式下,写者很可能长时间得不到一个写入的机会。读写公平模式下,读者和写者公平地参与一个common通用锁的竞争。

特点

  当读者正在读时,其余的读者仍可以自然进入读取过程(仍然需要排队以获取对计数器的互斥访问权限)。但是当写者试图写入时,**写者与读者加入一个共同的等待队列中,**前面的读者运行完成后,写者就开始写入。排在写者后面的读者,需要等待写入完成后才能开始读取

实现

设置信号量:

  • 互斥信号量wm(write mutex)写写/读写互斥锁, rcm(read count mutex)计数器锁,common读写通用锁,用于公平等待。
  • 计数器rc (read count),表示当前在读取的读者数。

实现代码如下:

mutex rcm = {1, NULL}, wm = {1, NULL}, common = {1, NULL};
counter rc = 0;

void reader()
{
	P(&common);		// add to common waiting list
	P(&rcm);		// acquire read counter lock
	if (rc == 0)	// the first reader?
		P(&wm);		// acquire writer mutex, wait for write progress done
	rc++;
	V(&rcm);		// release counter lock
	V(&common);
	// Do-read;
	P(&rcm);		// acquire again
	rc--;
	if (rc == 0)	// the last reader?
		V(&wm);		// release writer mutex, writer can acquire it
	V(&rcm);
}

void writer()
{
	P(&common);		// add to common waiting list
	P(&wm);			// acquire for writer mutex
	// Do-Write;
	V(&wm);
	V(&common);
}

分析要点
  • 公平,本质上是让读者和写者**在一个等待队列上等待。而信号量本质上是对等待队列的一个封装。**我们只需要让读者和写者共同请求一个信号量,就能达到让它们在一个等待队列上等待的目的。
一种优化

  开始,笔者想到的思路是,将信号量退化成一个单纯的等待队列。写者获取common锁后,读者进入等待队列。而读者不会持有common锁,它会在得到锁后立即释放,相当于仅做了一个对writer是否等待的测试。在这种思路下,将12行的V(&common)移动到6行P(&common),可以获得完全相同的运行结果。

  这样的改动可能带来的优化是,缩小了Reader前期的Critical Section块大小,也就是缩短了互斥模式下代码的长度,在并发情况下一定程度上可以提高并发效率。(本项为笔者理论推算,没有经过实验,如果有误,烦请指正)


错误修正:本优化方法错误。在Reader释放common锁后,如果有writer在等待,将导致它们无序竞争。在此鸣谢读者buxiangxiezuoyea的指正!

写者优先

特点

  当有写者试图写入时,所有等待的读者(即等待获取reader counter锁的读者)让后,让写者抢先写入。当没有任何读者试图写入时,读者仍可以不互斥地读取。

实现

设置信号量:

  • 互斥信号量wm(write mutex)写写/读写互斥锁, rcm(read count mutex)计数器锁,rm由写者持有的读者锁,wcm(write count mutex)写者计数器锁, rs(reader single)单写者锁
  • 计数器rc (read count),表示当前在读取的读者数。

实现代码如下:

void reader()
{
	P(&rs);
	P(&rm);		
	P(&rcm);		// acquire read counter lock
	if (rc == 0)	// the first reader?
		P(&wm);		// acquire writer mutex, wait for write progress done
	rc++;
	V(&rcm);		// release counter lock
	V(&rm);
	V(&rs);
	// Do-read;
	P(&rcm);		// acquire again
	rc--;
	if (rc == 0)	// the last reader?
		V(&wm);		// release writer mutex, writer can acquire it
	V(&rcm);
}

void writer()
{
	P(&wcm);		// acquire write counter lock
	if (wc == 0)	// the first writer?
		P(&rm);		// acquire reader mutex, block readers
	wc++;
	V(&wcm);
	P(&wm);			// wait for previous read / write
					// Do-write();
	V(&wm);
	P(&wcm);
	wc--;
	if (wc == 0)	// the last writer?
		V(&rm);		// release the reader mutex
	V(&wcm);
}
分析要点
  • 为什么写者优先情况下仍需要读者计数器?因为读取不具有并发性,可能有多个读者正在进行读取操作,写者需要等待所有正在读取的读者完成后才开始写入
  • 写者计数器:由于写者优先,我们需要记录所有互斥等待的写者数目,只有当没有写者等待时,才允许读者读取。
  • 读者锁rs:与读者优先类似,这个读者锁要求同时只能有一个读者参与请求rm。这样,当写者需要写入时,可以获取rm,以阻止后续排队的reader抢占rm。否则,大量提前进入的读者排队等待rm锁,写者必须排在后面,这与我们“写者优先”的预设不符。

总结归纳

等待队列

等待队列,本质上是对一个已经被占用的信号量(锁)请求时的等待过程。对应的等待事件为从该信号量被获取§到被释放(V)的过程。

综合上述三种模式的分析,我们对读者和写者等待队列的本质有了进一步的认识:

  • 写者的等待队列非常容易理解,由于写操作的互斥性,必须等一个写操作(或是某些模式下的读操作)完成了才能写入,自然地产生
  • 读者同样具有等待队列,读者的等待过程是等待rcm的过程,即,排队获取计数器的访问权限。当读者开始读操作后,由于没有互斥性, 读操作能够无互斥地并发完成。

实现优先

  • 优先,特性表现在一类行为必须在另一类行为所有都结束后,才开始执行。如果这类行为在排队,会被后来的另一类行为抢先执行。
  • 优先的实现主要由下列两个过程完成
    1. 增加高优先级方的计数器,在有高优先级行为时,获取一个对低优先级行为的锁,最后一个高优先级行为结束后,释放该锁。这个锁由高优先级行为控制,同时对低优先级行为互斥
      限。当读者开始读操作后,由于没有互斥性, 读操作能够无互斥地并发完成。

实现优先

  • 优先,特性表现在一类行为必须在另一类行为所有都结束后,才开始执行。如果这类行为在排队,会被后来的另一类行为抢先执行。
  • 优先的实现主要由下列两个过程完成
    1. 增加高优先级方的计数器,在有高优先级行为时,获取一个对低优先级行为的锁,最后一个高优先级行为结束后,释放该锁。这个锁由高优先级行为控制,同时对低优先级行为互斥
    2. 低优先级行为增加单个限制(利用互斥锁),同一时刻只允许单个线程请求1中描述的锁。这样避免了在该锁(也即信号量,本文中两个概念互换使用)上排有低优先级的长队列,使得高优先级行为无法抢占执行。
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值