1. 程式人生 > 其它 >C/C++ 讀寫鎖Readers-Write Lock

C/C++ 讀寫鎖Readers-Write 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的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_one();
		}
	}
	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]Stevens, W.Richard. UNIX network programming. Volume 2, Interprocess communications. UNIX網路程式設計. 卷2, 程序間通訊 / 3r[M]. 人民郵電出版社, 2009.
[2]https://en.wikipedia.org/wiki/Readers–writer_lock