zoukankan      html  css  js  c++  java
  • C/C++ 读写锁ReadersWriter 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

  • 相关阅读:
    数据类型对照表
    MySql
    操作MongoDB
    Linux常用命令
    MongoDB性能参数
    有关Remoting的几个结论与论证
    异步获取远程文件
    Bigtable:一个分布式的结构化数据存储系统(转)
    windbg命令
    C#下几种排序算法
  • 原文地址:https://www.cnblogs.com/fortunely/p/15778050.html
Copyright © 2011-2022 走看看