golang sync锁

本文详细介绍了Golang中的sync包,包括Mutex和RWMutex的使用。Mutex提供了互斥锁,保证同一时间只有一个协程访问资源,而RWMutex则支持读写锁,允许多个读取者同时访问。文章分析了Mutex的数据结构、加锁解锁的自旋操作以及解锁的流程,同时探讨了RWMutex的读写锁实现和唤醒机制。
摘要由CSDN通过智能技术生成

sync锁

一、Mutex

数据结构

type Mutex struct {
 state int32 // 表示当前互斥锁的状态
 sema  uint32 // 用于控制锁状态的信号量
}

state的基本信息:

  • Locked互斥锁的锁定状态

  • Woken表示从正常模式被从唤醒;

    当一个协程调用 Mutex.Lock() 时,如果 Mutex 已经被其他协程锁定,则该协程将被阻塞,并等待 Mutex 被释放。当其他协程调用 Mutex.Unlock() 释放 Mutex 时,如果等待的协程存在,则会设置 “woken” 标志,以通知等待的协程可以再次尝试获取 Mutex。这样,被阻塞的协程就可以继续执行下去,而不必一直等待下去。

  • Starving 当前的互斥锁进入饥饿状态;

  • waitersCount 当前互斥锁上等待的 Goroutine 个数;

模式

  • 普通模式
    • 锁的等待者会按照先进先出的顺序获取锁。
    • 刚被唤起的 Goroutine 与新创建的 Goroutine 竞争时,大概率会获取不到锁
  • 饥饿模式
    • 互斥锁会直接交给等待队列最前面的 Goroutine。
    • 新的 Goroutine 在该状态下不能获取锁、也不会进入自旋状态,只会在队列的末尾等待。
    • 如果一个 Goroutine 获得了互斥锁并且它在队列的末尾或者它等待的时间少于 1ms,那么当前的互斥锁就会切换回正常模式

加锁 Lock()

源码分析

func (m *Mutex) Lock() {
	// 当锁的状态是 0 时,将 mutexLocked 位置成 1
	if atomic.CompareAndSwapInt32(&m.state, 0, mutexLocked) {
		return
	}
	// 不是 0 时 尝试通过自旋(Spinnig)等方式等待锁的释放
	m.lockSlow()
}
  • 自旋操作的流程
    • 判断当前 Goroutine 能否进入自旋;
      • 互斥锁只有在普通模式才能进入自旋;
      • 运行在多 CPU 的机器上;
      • 当前 Goroutine 为了获取该锁进入自旋的次数小于四次;
      • 当前机器上至少存在一个正在运行的处理器 P 并且处理的运行队列为空;
    • 通过自旋等待互斥锁的释放;
    • 计算互斥锁的最新状态;
    • 更新互斥锁的状态并获取锁;

解锁 UnLock()

源码分析

func (m *Mutex) Unlock() {
	// 如果该函数返回的新状态等于 0,当前 Goroutine 就成功解锁了互斥锁;
	new := atomic.AddInt32(&m.state, -mutexLocked)
	// 不等于 0,开始慢速解锁:
	if new != 0 {
		m.unlockSlow(new)
	}
}
  • 慢速解锁的流程
    • 正常模式
      • 如果互斥锁不存在等待者或者互斥锁的 LockedStarvingWoken 状态不都为 0,直接返回,不需要唤醒其他等待者
      • 如果互斥锁存在等待者,会通过 sync.runtime_Semrelease 唤醒等待者并移交锁的所有权;
    • 饥饿模式
      • 将当前锁交给下一个正在尝试获取锁的等待者
      • 等待者被唤醒后会得到锁,在这时互斥锁还不会退出饥饿状态

总结

  • 如果互斥锁处于初始化状态,通过置位 Locked 加锁;
  • 如果当前 Goroutine 等待锁的时间超过了 1ms,互斥锁就会切换到饥饿模式;
  • 当互斥锁已经被解锁时,调用 sync.Mutex.Unlock 会直接抛出异常;

二、RWMutex

数据结构

type RWMutex struct {
 w           Mutex  // 复用互斥锁提供的能力;
 writerSem   uint32 // 写等待读
 readerSem   uint32 // 读等待写
 readerCount int32  // 存储了当前正在执行的读操作数量;读锁+1 写锁-1
 readerWait  int32  // 表示当写操作被阻塞时等待的读操作个数;
}

写锁

加锁 sync.RWMutex.Lock
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)
	}
}
  • 调用互斥锁,阻塞后续的写操作
  • 调研原子操作函数阻塞后续的读操作
  • 如果仍有其他Goroutine 持有互斥锁的读锁,该 Goroutine 会调用 runtime.sync_runtime_SemacquireMutex 进入休眠状态等待所有读锁所有者执行结束后释放 writerSem 信号量将当前协程唤醒;
解锁 sync.RWMutex.Unlock
func (rw *RWMutex) Unlock() {
	r := atomic.AddInt32(&rw.readerCount, rwmutexMaxReaders)
	if r >= rwmutexMaxReaders {
		throw("sync: Unlock of unlocked RWMutex")
	}
	for i := 0; i < int(r); i++ {
		runtime_Semrelease(&rw.readerSem, false, 0)
	}
	rw.w.Unlock()
}
  • 原子操作将 readerCount 变回正数,释放读锁
  • 通过 for 循环释放所有因为获取读锁而陷入等待的 Goroutine:
  • 释放互斥锁

读锁

加锁 sync.RWMutex.RLock
func (rw *RWMutex) RLock() {
	if atomic.AddInt32(&rw.readerCount, 1) < 0 {
		runtime_SemacquireMutex(&rw.readerSem, false, 0)
	}
}
  • 原子操作将 readerCount 加一
  • 如果原子操作返回的是负数说明其他Goroutine 获得了写锁,这个时候当前 Goroutine 就会调用 runtime.sync_runtime_SemacquireMutex 陷入休眠等待锁的释放
解锁sync.RWMutex.RUnlock
func (rw *RWMutex) RUnlock() {
	if r := atomic.AddInt32(&rw.readerCount, -1); r < 0 {
		rw.rUnlockSlow(r)
	}
}
  • 原子操作将 readerCount 减一
  • 如果返回值< 0说明这个时候有症状执行的写操作,这时会调用sync.RWMutex.rUnlockSlow 方法延迟解锁

总结

  • 尝试获取写锁时
    • 每次 sync.RWMutex.RUnlock都会将 readerCount 其减一,当它归零时该 Goroutine 会获得写锁
    • readerCount 减少 rwmutexMaxReaders 个数以阻塞后续的读操作;
  • 释放写锁的时候
    • 会先通知所有的读操作,然后才会释放持有的互斥锁;
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值