C/C++ 读写锁Readers-Writer Lock

读写锁基本概念

读写锁(readers-writer lock),又称为多读单写锁(multi-reader single-writer lock,或者MRSW lock),共享互斥锁(shared-exclusive lock),以下简称RW lock。
读写锁用来解决读写操作并发的问题。多个线程可以并行读取数据,但只能独占式地写或修改数据。

write-mode和read-mode
RW lock有两种模式:write-mode,read-mode。

  • write-mode
    在write-mode下,一个writer取得RW lock。当writer写数据时,其他所有writer或reader将阻塞,直到该writer完成写操作;
  • read-mode
    在read-mode下,至少一个reader取得RW lock。当reader读数据时,其他reader也能同时读取数据,但writer将阻塞,直到所有reader完成读操作;

RW lock升级与降级
当writer取得RW lock,进入write-mode,对数据进行写操作时,进入read-mode进行读操作。我们把这个称为锁降级(downgraded RW lock)。
当reader取得RW lock,进入read-mode,对数据进行读操作时,进入write-mode进行写操作。我们把这个称为锁升级(upgradable RW lock)。
锁降级是安全的;而锁升级是不安全的,容易造成死锁,应当避免。

读写锁与互斥锁的关系

相同点在于对写操作是互斥的。
主要区别在于锁的粒度,针对读操作,reader可以共享数据;而针对写操作,与其他任意reader或writer都是互斥的。
可以用互斥锁来实现读写锁。

读写锁与互斥锁的详细区别,可以参见这篇文章:Linux 自旋锁,互斥量(互斥锁),读写锁

优先级策略

针对reader与writer访问,RW lock能设计成不同的优先级策略:read-preferring(读优先),write-preferring(写优先),unspecified priority(不确定优先级)。

  • read-preferring,允许最大并发量,但如果争用较多时,将导致写饥饿:writer线程将长期不能完成写操作。因为只要有一个reader线程持有lock,writer就无法取得RW lock。而连续不断新来的reader,将导致writer长期无法取得RW lock。
  • write-preferring,能有效避免写饥饿问题,但相对地,会带来读饥饿问题。
  • unspecified priority,不保证优先读访问,或写访问。

接口

通常,RW lock需要对外提供以下接口:
1)初始化Initialize
2)销毁Destroy
3)取得读锁,进入read-mode
4)释放读锁,退出read-mode
5)取得写锁,进入write-mode
6)释放写锁,退出write-mode

linux的POSIX pthread线程库中的pthread_rwlock是RW lock的一个实现,其接口为:

#include <pthread.h>

int pthread_rwlock_destroy(pthread_rwlock_t *rwlock);   /* 销毁RW lock */
int pthread_rwlock_init(pthread_rwlock_t *restrict rwlock,
      const pthread_rwlockattr_t *restrict attr);       /* 初始化RW lock */

pthread_rwlock_t rwlock = PTHREAD_RWLOCK_INITIALIZER;   /* 直接赋值方式初始化RW lock */

int pthread_rwlock_rdlock(pthread_rwlock_t *rwlock);    /* 取得读锁,进入read-mode */
int pthread_rwlock_tryrdlock(pthread_rwlock_t *rwlock); /* 尝试取得读锁,失败立即返回  */

int pthread_rwlock_trywrlock(pthread_rwlock_t *rwlock); /* 取得写锁,进入write-mode */
int pthread_rwlock_wrlock(pthread_rwlock_t *rwlock);    /* 尝试取得写锁,失败立即返回  */

int pthread_rwlock_unlock(pthread_rwlock_t *rwlock);    /* 释放读/写锁 */

实现

如果是在linux中,我们可以直接使用pthread线程库的pthread_rwlock。而如果是其他平台,如Win32,就需要自行实现读写锁。
注:C++17中,std::shared_lock支持RW lock。

RW lock实现有多种方式,其中代表性的有两种:

使用2个mutex

要求:2个mutex,1个int计数器。
其中,计数器b记录阻塞等待的reader数量。1个mutex r,用来保护b只被reader使用;另外1个mutex(全局的)确保writers互斥。用英文来解释:
b: a counter, tracking for number of readers waiting RW lock;
r: a mutex, protect b and only used by readers;
g: a global mutex, ensure mutual exclusion of writers. It can be acquired by one thread, but released by another.

伪代码

  • 初始化Initialize
Set b to 0; /* clear counter b */
r is unlocked; /* init mutex r */
g is unlocked; /* init mutex g */
  • 取得读锁Begin Read
Lock r; // 注意这里的r,只是用来锁住RW lock内部资源
b++;
if b = 1, lock g; // g的lock线程和unlock线程可能并非同一个
Unlock r;
  • 释放读锁End Read
Lock r;
b--;
if b = 0, unlock g;
Unlock r;
  • 取得写锁Begin Write
Lock g; // 只有处于write-mode时,对g进行unlock和lock的才要求是同一个线程
  • 释放写锁End Write
Unlock g;

这种方式一个具体的实现,可参见:41 C++ 读写锁的实现及使用样例 | 知乎

使用1个condition variable + 1个mutex

要求:1个condition variable(条件变量)cond,1个普通mutex g,若干个计数器、标志,用于表示线程当前处于激活或阻塞状态。
1)num_readers_active,取得lock的readers数量;
2)num_writers_waiting,阻塞等待lock的writers数量;
3)writer_active,表示一个writer是否已经取得lock;

伪代码

  • 取得读锁Begin Read

采用写优先方式(write-preferring),会影响到加锁方式。

Lock g;
while num_writers_waiting > 0 or writer_active: /* 等待所有writer */
	wait cond, g; /* 等待条件变量cond, 释放互斥锁g */
num_readers_active++;
Unlock g;
  • 释放读锁End Read
Lock g;
num_readers_active--;
if num_readers_active == 0:
	Notify cond(broadcast) /* why not signal? */
Unlock g;

思考:为什么这里条件变量唤醒用的是broadcast(广播,唤醒所有),而不是signal(唤醒单个)?
答:个人认为,broadcast和signal效果是一样的。首先,能运行这段代码,说明已经之前已经取得了read-lock,处于read-mode,现在是准备释放read-lock。也就是说,已经等待条件变量cond上的线程,只可能是writer(因为之前的reader会立即取得read lock)。使用signal是随机唤醒一个write线程,接着直接取得写锁;而使用broadcast会唤醒所有write线程,再通过下面的取得写锁来争用。

  • 取得写锁Begin Write
Lock g;
num_writers_waiting++;
while num_readers_active > 0 or writer_active is true: /* 等待所有readers或其他writer */
	wait cond, g;
num_writers_waiting--;
Set writer_active to true;
Unlock g;
  • 释放写锁End Write
Lock g;
Set writer_active to false;
Notify cond(broadcast);
Unlock g;

使用1个mutex + 2个条件变量

问题:能否将释放读锁和释放写锁写在同一个函数中?
就像POSIX的pthread_rwlock_unlock一样,不论持有的是读锁,还是写锁,解锁操作都是一个,我们也可以把两者设计到同一个接口中。参照UNP卷2,我们写出读写锁的C++版本实现:1个mutex + 2个条件变量。

实现RW lock完整代码:

class RWLock {
public:
	RWLock() : rw_nwaitreaders(0), rw_nwaitwriters(0), rw_refcount(0) { } 
	~RWLock() = default;
	RWLock(const RWLock&) = delete;
	RWLock& operator=(const RWLock&) = delete;

public:
	void rdlock();    /* wait for reader lock */
	bool tryrdlock(); /* try to get reader lock */
	void wrlock();    /* wait for writer lock */
	bool trywrlock(); /* try to get writer lock */
	void unlock();    /* release reader or writer lock */

private:
	std::mutex rw_mutex;
	std::condition_variable_any rw_condreaders;
	std::condition_variable_any rw_condwriters;
	int rw_nwaitreaders;                        /* the number of waiting readers */
	int rw_nwaitwriters;                        /* the number of waiting writers */
	int rw_refcount; /* 0: not locked; -1: locked by one writer; > 0: locked by rw_refcount readers */
};
// 阻塞获取读锁
void RWLock::rdlock()
{
	rw_mutex.lock();
	{
		/* give preference to waiting writers */
		while (rw_refcount < 0 || rw_nwaitwriters > 0) { // 写优先
			rw_nwaitreaders++;
			rw_condreaders.wait(rw_mutex);
			rw_nwaitreaders--;
		}
		rw_refcount++;  /* another reader has a read lock */
	}
	rw_mutex.unlock();
}
// 尝试获取读锁,失败立即返回
bool RWLock::tryrdlock()
{
	bool res = true;
	rw_mutex.lock();
	{
		if (rw_refcount < 0 || rw_nwaitwriters > 0) { // 写优先
			res = false; /* held by a writer or waiting writers */
		}
		else {
			rw_refcount++; /* increment count of reader locks */
		}
	}
	rw_mutex.unlock();
	return res;
}
// 阻塞获取写锁
void RWLock::wrlock()
{
	rw_mutex.lock();
	{
		while (rw_refcount != 0) { /* wait other readers release the rd or wr lock */
			rw_nwaitwriters++;
			rw_condwriters.wait(rw_mutex);
			rw_nwaitwriters--;
		}
		rw_refcount = -1; /* acquire the wr lock */
	}
	rw_mutex.unlock();
}
// 尝试获取写锁,失败立即返回
bool RWLock::trywrlock()
{
	bool res = true;
	rw_mutex.lock();
	{
		if (rw_refcount != 0) /* the lock is busy */
			res = false;
		else
			rw_refcount = -1; /* acquire the wr lock */
	}
	rw_mutex.unlock();
	return res;
}
// 释放写锁或读锁
void RWLock::unlock()
{
	rw_mutex.lock();
	{
		if (rw_refcount > 0)
			rw_refcount--;
		else if (rw_refcount == -1)
			rw_refcount = 0;
		else
			// unexpected error
			fprintf(stderr, "RWLock::unlock unexpected error. rw_refcount = %d\n", rw_refcount);
		
		/* give preference to waiting writers over waiting readers */
		if (rw_nwaitwriters > 0) {
			if (rw_refcount == 0) {
				rw_condwriters.notify_one();
			}
		}
		else if (rw_nwaitreaders > 0) {
			rw_condreaders.notify_all(); /* rw lock is shared */
		}
	}
	rw_mutex.unlock();
}

测试程序

#include <thread>
#include <mutex>
#include <iostream>
#include <vector>

using namespace std;

volatile int v = 0;
RWLock rwlock;

void WriteFunc()
{
	this_thread::sleep_for(chrono::milliseconds(10)); // 为了演示效果,先让write线程休眠10ms
	rwlock.wrlock();
	{
		v++;
		cout << "Write:" << v << endl;
	}
	rwlock.unlock();
}

void ReadFunc()
{
	rwlock.rdlock();
	{
		cout << "Read:" << v << endl;
	}
	rwlock.unlock();
}

void test_rwlock()
{
	vector<thread> writers;
	vector<thread> readers;

	for (int i = 0; i < 20; ++i) {
		writers.push_back(thread(WriteFunc));
	}
	for (int i = 0; i < 200; ++i) {
		readers.push_back(thread(ReadFunc));
	}

	for (auto & e : writers) {
		e.join();
	}
	for (auto & e : readers) {
		e.join();
	}

	getchar();
}

小结

1)要注意区分RW lock的的设计者和使用者,使用lock时的区别
对于使用者,RW lock保护的是用户自定义资源,就像这样

rwlock.wrlock();
自定义资源 // RWLock保护用户自定义资源
rwlock.unlock();

而对于设计者,RW lock需要保护内部数据的线程安全,因此必须使用mutex在每次修改内部状态时,先加锁,然后解锁。像这样,

RWLock::rdlock()
{
	mutex.lock(); // 确保RWLock内部数据的修改是线程安全的
	修改RWLock内部数据
	mutex.unlock();
}

参考

[1]Stevens, W.Richard. UNIX network programming. Volume 2, Interprocess communications. UNIX网络编程. 卷2, 进程间通信 / 3r[M]. 人民邮电出版社, 2009.
[2]https://en.wikipedia.org/wiki/Readers–writer_lock

posted @ 2022-01-08 20:00  明明1109  阅读(7254)  评论(0编辑  收藏  举报