golang-sync.RWMutex(读写锁)源码刨析

433 阅读5分钟

信号量&PV操作

什么是PV操作

在操作系统中,一个进程的状态一般可以分为以下三个状态,三个状态的转换是有PV操作来控制的。PV操作主要是P操作(申请资源),V操作(释放资源)和信号量。

PV操作的意义

什么是信号量

信号量也被称为信号灯,是并发编程中一种常见的同步机制,在需要控制资源的线程访问数量时使用。信号量的结构是一个值和一个指针,指针指向下一个要被执行的进程,信号量的值和相应的资源数量有关。

  • 一般来说我们用信号量保护一组资源,比如数据库链接池,一组客户端的链接等等。每次获取资源都会将信号量中的计数器减去对应的值,在释放资源时重新加回来。当信号量没有资源时尝试获取信号量的线程就会进入休眠,等待其他线程释放信号量。当其他线程释放了资源的时候,对应挂起的线程会被唤起继续执行。

go语言中的信号量实现

自旋锁

什么是自旋锁

自旋锁是指一个线程在获取锁的时候,如果锁已经被其他线程占有。那么当前线程会循环等待锁的释放,当获取到锁的时候就退出循环。 正如其名,自旋锁一直占有着cpu,但是并没有执行有效的任务,造成了资源的浪费。因此自旋锁在实现时会有一个最多持续尝试的次数或者占用CPU时间片超时时间。超过后,自旋锁放弃当前时间片,等待下一次获取。

自旋锁与互斥锁

自旋锁和互斥锁比较类似,都是防止某项资源的互斥使用。自旋锁和互斥锁在使用时,都是只能有一个持有者,但是两者在调度策略上不同。对于互斥锁,如果资源已被占用,那么资源申请线程会被阻塞。但是对于自旋锁,如果自旋锁已被其他线程占有,那资源申请线程会一直循环检测。

自旋锁的使用场景

自旋锁比较适用于锁使用者保持锁时间比较短的情况。正是由于自旋锁使用者一般保持锁时间非常短,因此选择自旋而不是睡眠是非常必要的,自旋锁的效率远高于互斥锁。信号量和读写信号量适用于保持时间较长的情况,它们会导致调用者睡眠,因此只能在进程上下文使用,而自旋锁适用于保持时间非常短的情况,它可以在任何上下文使用

cas算法

什么是cas算法

cas算法是一种无锁算法,即再不加锁的情况下实现多线程之间的变量同步。不加锁,即不阻塞进程的情况下同步。cas算法涉及三个操作数:

  • 内存中的值(V)
  • 预期的值(S)
  • 写入的新值(N) 当且仅当V == S的时候,CAS通过原子方式用新值N来更新V的值,否则不会进行任何操作,一般情况下自旋操作即不断重试。

RWMutex大体结构

type RWMutex struct {
	w           Mutex  // 互斥锁,控制多个写入操作的并发执行
	writerSem   uint32 // 写入操作的信号量
	readerSem   uint32 // 读操作的信号量
	readerCount int32  // 当前读操作的个数
	readerWait  int32  // 当前写入操作需要等待读操作解锁的个数
}

Rlock - 获取读锁

// 
func (rw *RWMutex) RLock() {
	// 位置1
	if race.Enabled {
		_ = rw.w.state
		race.Disable()
	}
   	// 位置2
	if atomic.AddInt32(&rw.readerCount, 1) < 0 {
		runtime_SemacquireMutex(&rw.readerSem, false)
	}
	if race.Enabled {
		race.Enable()
		race.Acquire(unsafe.Pointer(&rw.readerSem))
	}
}

位置1部分的race操作是golang中的竞态检测部分,这部分我们不重点看,有兴趣的可以了解下golang的竞态检测。

位置2部分,我们可以看到每来一个协程获取读锁的时候,就将readerCount + 1。 但是当readerCount + 1 小于0的时候,就会执行runtime_SemacquireMute这样一个runtime的方法。runtime_SemacquireMute方法和runtime_Semacquire方法比较类似,如果readerCount + 1小于0, 则将当前的goroutine塞入信号量readerSem关联的oroutine waiting list,并休眠。

这里readerCount + 1小于0,是因为当前有写入操作的锁,写入的锁会将readerCount变成一个非常大的负数,所以获取读锁的协程就会被阻塞。

Lock - 获取写锁

func (rw *RWMutex) Lock() {
	if race.Enabled {
		_ = rw.w.state
		race.Disable()
	}
	// 位置1
	rw.w.Lock()
	// 位置2 rwmutexMaxReaders = 1 << 30 
	r := atomic.AddInt32(&rw.readerCount, -rwmutexMaxReaders) + rwmutexMaxReaders
	// 位置3
	if r != 0 && atomic.AddInt32(&rw.readerWait, r) != 0 {
		runtime_SemacquireMutex(&rw.writerSem, false)
	}
	if race.Enabled {
		race.Enable()
		race.Acquire(unsafe.Pointer(&rw.readerSem))
		race.Acquire(unsafe.Pointer(&rw.writerSem))
	}
}

位置1,这里是个互斥锁,至于互斥锁是什么原理可以移步(juejin.cn/editor/draf… 这里暂不做讨论,我们只需要知道互斥锁只有一个协程可以拿到。

位置2,这里我们可以从源码中看出,readerCount由于每有一个协程获取读锁就+1,一直都是正数,而当有写锁过来的时候,就瞬间减为很大的负数。位置2运行完后,r还是原来的readerCount。

位置3,这里在进行判断,如果r不等于0,证明原来已经有协程获取到了读锁。readerWait加上readerCount表示需要等待readerCount这么多个读锁进行解锁。上述判断证明原来有读锁,获取写锁的协程需要等待,所以调用runtime_SemacquireMutex这个runtime方法, 将当前的goroutine塞入信号量writerSem关联的goroutine wait list,并休眠。

Runlock - 释放读锁

func (rw *RWMutex) RUnlock() {
	if race.Enabled {
		_ = rw.w.state
		race.ReleaseMerge(unsafe.Pointer(&rw.writerSem))
		race.Disable()
	}
   	// 位置1
	if r := atomic.AddInt32(&rw.readerCount, -1); r < 0 {
    		// 位置2
		if r+1 == 0 || r+1 == -rwmutexMaxReaders {
			race.Enable()
			throw("sync: RUnlock of unlocked RWMutex")
		}
        	// 位置3
		if atomic.AddInt32(&rw.readerWait, -1) == 0 {
			runtime_Semrelease(&rw.writerSem, false)
		}
	}
	if race.Enabled {
		race.Enable()
	}
}

位置1,释放读锁,正常情况下给readerCount - 1,相减后的值为r,如果r 小于 0我们需要进行异常情况的判断。如果r > 0,那就是正常的情况。

位置2,这里r + 1 == 0 ,表示之前就没有读锁,在释放锁之前必须先获取读锁,如果获取不到读锁就去释放,那就异常了。同理 r+1 == -rwmutexMaxReaders 如果获取到了写锁就去释放也是会报异常。

位置3,除去异常情况,那就证明确实有协程正在获取写锁,那么就需要我们操作前面的readerWait, 如果readerWait + 1 == 0 ,表示没有人持有写锁,我们可以调用runtime_Semrelease这个runtime方法通过信号量writerSem的变化唤醒阻塞的goroutine(想要获取写锁的协程)你可以进行获取了。

runtime_Semrelease这个runtime方法会将阻塞在信号量上goroutine唤醒。

Unlock - 释放写锁

func (rw *RWMutex) Unlock() {
	if race.Enabled {
		_ = rw.w.state
		race.Release(unsafe.Pointer(&rw.readerSem))
		race.Disable()
	}

	// 位置1
	r := atomic.AddInt32(&rw.readerCount, rwmutexMaxReaders)
	if r >= rwmutexMaxReaders {
		race.Enable()
		throw("sync: Unlock of unlocked RWMutex")
	}
	// 位置2
	for i := 0; i < int(r); i++ {
		runtime_Semrelease(&rw.readerSem, false)
	}
	// 位置3
	rw.w.Unlock()
	if race.Enabled {
		race.Enable()
	}
}

位置1,在获取写锁的时候,会给readerCount - rwmutexMaxReaders,这个时候在释放的时候就需要加回来了。如果加回来的r 比rwmutexMaxReaders本身大或者相等,那还是没有获取到写锁就想去释放写锁,这时也会异常。

位置2,在Rlock中因为有写锁的存在,所以当前的想要获取读锁的协程都被阻塞了。阻塞的同时readerCount + 1,在位置1部分已经加上了rwmutexMaxReaders, 所以这个时候r就是被阻塞的获取读锁的goroutine的数量。 循环r次通过readerSem唤醒这些被阻塞的gorouteine。

位置3,在Lock中有加一个互斥锁,这里就需要释放了。

总结

至此,读写锁的分析基本上告一段落了。 有不足的地方大家可以指出来哦!另外下一篇将会分析下golang中的互斥锁(juejin.cn/editor/draf…