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