Go语言 读写锁&互斥锁原理剖析(1)

我们在多协程操作时,有种场景是读操作次数远远大于写操作,这个时候,我们就会考虑用到读写锁。

读写锁

读写锁(百科)定义:是一种特殊的的自旋锁,它把对共享资源的访问者划分成读者和写者,读者只对共享资源进行读访问,写者则需要对共享资源进行写操作。

在Go语言中、sync标准库定义了RWMutex结构代表读写锁,该结构在rwmutex.go中,RWMutex继承于Locker接口,其实是互斥锁的改进版,为什么这样说呢,看完下文就会清楚了。

读写锁特征:

读锁之间是共享的,多协程可以同时加读锁,写锁与读锁之间是互斥的。

源码基于:go version go1.13.4 windows/amd64。

Locker接口

type Locker interface {
	Lock()
	Unlock()
}

读写锁结构体定义

type RWMutex struct {
	w           Mutex  // 互斥锁,写锁协程获取该锁后,其他写锁处于阻塞等待
	writerSem   uint32 // 写入等待信号量,最后一个读取协程释放锁时会释放信号量
	readerSem   uint32 // 读取等待信号量,持有写锁协程释放后会释放信号量
	readerCount int32  // 读锁的个数
	readerWait  int32  // 写操作时,需要等待读操作的个数
}

我们看到RWMutex读写锁里面包含:

  1. w Mutex互斥锁,w控制并发时多个写操作
  2. writerSem 写入等待信号量
  3. readerSem 读取等待信号量
  4. readerCount 读锁的个数
  5. readerWait 写操作时,需要等待读操作的个数

再看常量rwmutexMaxReaders,定义的是最大读锁数量

// rwmutexMaxReaders:支持1073741824个锁
const rwmutexMaxReaders = 1 << 30
sync.RWMutex所有方法如下图所示

Go语言 读写锁&互斥锁原理剖析(1)_第1张图片

读写锁的核心方法:其主要核心的有Lock、Unlock、RLock和RUnlock四个方法。

func (*RWMutex) Lock    // 获取写锁操作
func (*RWMutex) Unlock  // 解除写锁操作
func (*RWMutex) RLock   // 获取读锁操作
func (*RWMutex) RUnlock // 解除读锁操作
RLocker() Locker        // 返回Locker对象

 

RLocker,返回一个Locker对象

// 返回一个Locker对象
func (rw *RWMutex) RLocker() Locker {
	return (*rlocker)(rw)
}

Lock,获取写锁操作

func (rw *RWMutex) Lock() {
	// ①、竞争锁时检测
	if race.Enabled {
		_ = rw.w.state
		race.Disable()
	}
	// 先获取Mutex互斥锁,多协程时只能有一个协程拿到锁
	rw.w.Lock()
	// ②、写锁时,会将readerCount减去rwmutexMaxReaders,
	// 设置需要等待释放的读锁的数量,如有则挂起获取读锁的协程
	r := atomic.AddInt32(&rw.readerCount, -rwmutexMaxReaders) + rwmutexMaxReaders
	if r != 0 && atomic.AddInt32(&rw.readerWait, r) != 0 {
        	// ③如果先进来的,会在队列前面阻塞等待,进入队列等待
		runtime_SemacquireMutex(&rw.writerSem, false, 0)
	}
	if race.Enabled {
		race.Enable()
		race.Acquire(unsafe.Pointer(&rw.readerSem))
		race.Acquire(unsafe.Pointer(&rw.writerSem))
	}
}

①写锁操作只能被一个写协程拿到,竞争检测后进行互斥锁上锁Mutex.lock,,多协程时只能有一个协程拿到锁

②写锁时,readerCount减去rwmutexMaxReaders,readerCount会变成很大的负数,读锁时readerCount会+1

③如果readerCount不等于0,协程获取到读锁,如果满足上锁条件时,会调用runtime_SemacquireMutex,如果先进来的会在队列前面阻塞等待的信号量为writerSem,进入队列等待。

Unlock,解除写锁操作

func (rw *RWMutex) Unlock() {
	// ①竞争检测
	if race.Enabled {
		_ = rw.w.state
		race.Release(unsafe.Pointer(&rw.readerSem))
		race.Disable()
	}

	// ②需要回复readerCount,上锁的时候减了常量rwmutexMaxReaders,这里再加回来
	r := atomic.AddInt32(&rw.readerCount, rwmutexMaxReaders)
	// ③对未被写锁定的读写锁进行写解锁,会报错
	if r >= rwmutexMaxReaders {
		race.Enable()
		throw("sync: Unlock of unlocked RWMutex")
	}
	// ④呼唤阻塞的读操作
	for i := 0; i < int(r); i++ {
		runtime_Semrelease(&rw.readerSem, false, 0)
	}

	// ⑤释放锁,如果有其他阻塞写操作,在此唤醒
	rw.w.Unlock()
	if race.Enabled {
		race.Enable()
	}
}

①解除写锁操作时,竞争检测

②需要回复readerCount,上锁的时候减了常量rwmutexMaxReaders,这里再加回来,加完之后如果大于等于rwmutexMaxReaders

③对未被写锁定的读写锁进行写解锁,会抛异常

④呼唤阻塞的读操作

⑤释放锁,如果有其他阻塞写操作,在此唤醒

RLock,获取读锁操作

func (rw *RWMutex) RLock() {
	if race.Enabled {
		_ = rw.w.state
		race.Disable()
	}
	// ①读锁时readerCount+1操作,当值小于0时会调用runtime_SemacquireMutex,表明写锁操作正在等待
	if atomic.AddInt32(&rw.readerCount, 1) < 0 {
		// 当写锁操作时,读锁也会阻塞等待
		runtime_SemacquireMutex(&rw.readerSem, false, 0)
	}
	if race.Enabled {
		race.Enable()
		race.Acquire(unsafe.Pointer(&rw.readerSem))
	}
}

①①读锁时readerCount+1操作,当值小于0时会调用runtime_SemacquireMutex,表明写锁操作正在等待

②当写锁操作时,读锁也会阻塞等待,读锁加入等待队列

RUnlock,解除读锁操作

func (rw *RWMutex) RUnlock() {
	if race.Enabled {
		_ = rw.w.state
		race.ReleaseMerge(unsafe.Pointer(&rw.writerSem))
		race.Disable()
	}
	// ①直接将readerCount-1操作
	if r := atomic.AddInt32(&rw.readerCount, -1); r < 0 {
		// ②如果r小于0,说明有协程正在获取写操作锁,
		// 当readerWait减到0时说明没有协程持有写操作锁,
		// 就通过信息号wrirerSem通知等待的协程来争抢锁
		rw.rUnlockSlow(r)
	}
	if race.Enabled {
		race.Enable()
	}
}

①解除读锁,直接对readerCount-1操作,这里就是把之前+1操作的再减回来

②如果r小于0,说明有协程正在获取写操作锁,当readerWait减到0时说明没有协程持有写操作锁,就通过信息号wrirerSem通知等待的协程来争抢锁

总结

  • 写锁操作时,对读写锁进行读锁定和写锁定,都会阻塞,读锁与写锁之间是互斥的。
  • 读锁操作时,对读写锁进行写锁定,会阻塞,加读锁时不会阻塞。
  • 对未被上锁的读|写锁进行解除锁定操作,会Panic。
  • 解除写锁操作时,同时会唤醒所有阻塞的读锁协程。
  • 解除读锁操作时,会唤醒一个因写锁操作而被阻塞的协程。
  • 读锁存在的时候,同时出现读锁和写锁操作,优先获取写锁。
  • 同时2个协程去争抢读锁和写锁时,都有机会抢成功。
  • 写锁存在时,同时开启2个协程去争抢读锁和写锁,优先获得读锁。

本文为原创文章,出自guichenglin,转载请粘贴源链接,如果未经允许转发后果自负。

你可能感兴趣的:(golang)