读写锁的定义

  • 读操作与写操作互斥;
  • 写操作与写操作互斥;
  • 读操作可以并发;

主要的API:

Golang读写锁的实现

golang读写锁的结构体如下:

go version go1.16.4 darwin/amd64
const rwmutexMaxReaders = 1 << 30

无竞争状态下加解锁

Lock && Unlock

func (rw *RWMutex) Lock() {
    rw.w.Lock()
    r := atomic.AddInt32(&rw.readerCount, -rwmutexMaxReaders) 
        + rwmutexMaxReaders
    if r != 0 && atomic.AddInt32(&rw.readerWait, r) != 0 {
        runtime_SemacquireMutex(&rw.writerSem, false, 0)
    }
 }

执行完Lock之后,状态变化为:

那unlock也比较好理解了,还原就可以了;

func (rw *RWMutex) Unlock() {
    r := atomic.AddInt32(&rw.readerCount, rwmutexMaxReaders)
    for i := 0; i < int(r); i++ {
        runtime_Semrelease(&rw.readerSem, false, 0)
    }
    rw.w.Unlock()
}

Rlock && Runlock

func (rw *RWMutex) RLock() {
    if atomic.AddInt32(&rw.readerCount, 1) < 0 {
        runtime_SemacquireMutex(&rw.readerSem, false, 0)
    }
}

执行了一次或者多次rlock后:

再看下runlock:

func (rw *RWMutex) RUnlock() {
    if r := atomic.AddInt32(&rw.readerCount, -1); r < 0 {
        if atomic.AddInt32(&rw.readerWait, -1) == 0 {
            runtime_Semrelease(&rw.writerSem, false, 1)
        }
    }
}

可以看到runlock就是将readerCount恢复原状;

无竞争状态下读写锁的简单总结如下:

竞争状态下的加解锁

Lock状态下执行Lock与Rlock

func (rw *RWMutex) Lock() {
    rw.w.Lock()
    r := atomic.AddInt32(&rw.readerCount, -rwmutexMaxReaders) 
        + rwmutexMaxReaders
    if r != 0 && atomic.AddInt32(&rw.readerWait, r) != 0 {
        runtime_SemacquireMutex(&rw.writerSem, false, 0)
    }
 } 

func (rw *RWMutex) RLock() {
    if atomic.AddInt32(&rw.readerCount, 1) < 0 {
        runtime_SemacquireMutex(&rw.readerSem, false, 0)
    }
}

在lock下,可以看到

  • 所有后续的lock全部阻塞在 rw.w.lock()上;
  • 通过状态:rc小于0 标示锁被写占用, 使得后续的读阻塞在信号量readerSem上;
  • 同时rc 为 -rwmutexMaxReaders+n, n为被阻塞的读操作的数量;

执行unlock时:

func (rw *RWMutex) Unlock() {
    r := atomic.AddInt32(&rw.readerCount, rwmutexMaxReaders)
    for i := 0; i < int(r); i++ {
        runtime_Semrelease(&rw.readerSem, false, 0)
    }
    rw.w.Unlock()
}

总结下:在写锁先占用的情况下:

  • 写锁释放时,
    • 所以:读写锁的状态变化为:写占用-读占用
    • 在写锁期间,被阻塞的读操作一定先执行:写->读
    • 读锁并不知道写操作阻塞的情况,见缝插针的执行;

接下来我们再看下这m个被阻塞的写操作如何翻身,又用到了哪些属性,以及读写锁:写-读 之后的下一个状态是什么,一定是读?一定是写,还是读写都有可能?

Rlock状态下执行Lock与Rlock

已经rlock了n次;

func (rw *RWMutex) Lock() {
    rw.w.Lock()
    r := atomic.AddInt32(&rw.readerCount, -rwmutexMaxReaders) 
            + rwmutexMaxReaders
    if r != 0 && atomic.AddInt32(&rw.readerWait, r) != 0 {
        runtime_SemacquireMutex(&rw.writerSem, false, 0)
    }
}
func (rw *RWMutex) RLock() {
    if atomic.AddInt32(&rw.readerCount, 1) < 0 {
        runtime_SemacquireMutex(&rw.readerSem, false, 0)
    }
}

此时,执行runlock会怎么样:

func (rw *RWMutex) RUnlock() {
    if r := atomic.AddInt32(&rw.readerCount, -1); r < 0 {
        if atomic.AddInt32(&rw.readerWait, -1) == 0 {
            runtime_Semrelease(&rw.writerSem, false, 1)
        }
    }
}

此时,又变为lock下的lock与rlock了;

总结下:在读锁先占用的情况下

  • 写操作通过状态:rc < 0 标识有写等待;这样后续的读被阻塞;
  • 读释放时:发现写等待,就会唤醒写;
  • 因此:读占用时,读写锁的状态转化为:读占用-写占用;

golang 读写锁的总结:

  • 在读占用的情况下,写操作的到来将读操作划分成了 已经占用锁的读操作和未占用读锁的读操作两部分;待已占用的读释放完全,再唤醒写;
  • 在写占用的情况下,写操作释放锁时,唤醒写锁期间到来的读操作,然后再释放写锁;
  • 将上面的状态转化连起来,推论:在读写高并发时,golang的实现就变为:写(1),读(n),写(1),读(j)……
  • 有一个遗留的问题:m+z的lock操作都阻塞在 rw.w.lock() 上,如何确定下一个w写锁由谁获取?
    • 按照写操作来的顺序?
    • 最新的写操作?
    • Both?
    • 即: java读写锁的公平模式与非公平模式
type RWMutex struct {
    w           Mutex  // held if there are pending writers
    writerSem   uint32 // semaphore for writers to wait for completing readers
    readerSem   uint32 // semaphore for readers to wait for completing writers
    readerCount int32  // number of pending readers
    readerWait  int32  // number of departing readers
}


C++读写锁的实现简单分析
stl的shared_mutex

class shared_mutex {
    std::mutex mut_;
    std::condition_variable gate1_;
    std::condition_variable gate2_;
    unsigned state_;

    /* example:
       EXCLUSIVE_WAITING_BLOCKED_MASK == 0x80000000;
       MAX_SHARED_COUNT_MASK == 0x7fffffff;
       NO_EXCLUSIVE_NO_SHARED == 0x00000000;
     */

public:
    shared_mutex() : state_(NO_EXCLUSIVE_NO_SHARED) {}
    // Exclusive ownership
    void lock();
    void unlock();
    // Shared ownership
    void lock_shared();
    void unlock_shared();
};


runlock(): 如果gate2有阻塞,则触发gate2;
unlock(): gate1.notifyall()

boost的shared_mutex

class shared_mutex {
    struct state_data {
        unsigned shared_count; // 读者数量
        bool exclusived;      // 表示加了写锁
        bool exclusive_entered; //表示即将加写锁、
        // bool upgrade // etc
    };
    state_data m_state;
    boost::mutex m_mutex_state;  // 对应golang的原子操作?
    boost::condition_variable m_shared_cond;// readSem?



unlock时 或者unlock_shared()时:
        m_exclusive_cond.notify_one()
        m_shared_cond.notify_all()

Ps: boost提供读锁升级到写锁;

Java读写锁的实现的简单分析

public KReentrantReadWriteLock(boolean fair){
 sync = fair ? new FairSync() : new NonfairSync(); 
 readerLock = new ReadLock(this); 
 writerLock = new WriteLock(this); } 
 }
 非公平锁:
 readshouldblock: head.next是否是写锁
 writeshouldblock: false
 公平锁:
 readshouldblock: hasQue
 writeshouldblock: hasQue




一些额外的特性:

  • 锁的可重入性;
  • 锁降级:获取写锁之后,可以降为读锁;
  • 锁的公平性与非公平性:非公平锁与公平锁;

读写锁实现总结



参考:

读写锁ReentrantReadWriteLock源码分析_ThinkWon的博客-CSDN博客

C++并发型模式#7: 读写锁 - shared_mutex | 邓作恒的博客

读写锁ReadWriteLock的实现原理