Golang的锁机制
学习过操作系统的都知道程序有临界区这个概念,临界区就是程序片段访问临界资源的那部分代码,临界区同一时刻只能有一个线程进行访问,其他线程需要访问的话必须等待资源空闲。那么一般编程语言都会使用锁来进行临界区访问控制。
sync.Mutex 和 sync.RWMutex
Golang 中的有两种锁,为 sync.Mutex 和 sync.RWMutex。
- sync.Mutex 互斥锁,只有一种锁:Lock(),它是绝对锁,同一时间只能有一个锁。
- sync.RWMutex 读写锁,它有两种锁: RLock() 和 Lock():
RLock() 叫读锁。实际是一种特殊的自旋锁,它把对共享资源的访问者划分成读者和写者,读者只对共享资源进行读访问,写者则需要对共享资源进行写操作。这种锁相对于自旋锁而言,能提高并发性,因为在多处理器系统中,它允许同时有多个读者来访问共享资源,最大可能的读者数为实际的逻辑CPU数。
Lock() 叫写锁,它是个绝对锁,就是说,如果一旦某人拿到了这个锁,别人就不能再获取此锁了。
底层方法
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
}
type Mutex struct {
state int32
sema uint32
}
func (rw *RWMutex) Lock() {
if race.Enabled {
_ = rw.w.state
race.Disable()
}
// First, resolve competition with other writers.
rw.w.Lock()
// Announce to readers there is a pending writer.
r := atomic.AddInt32(&rw.readerCount, -rwmutexMaxReaders) + rwmutexMaxReaders //rwmutexMaxReaders: 1 << 30
// Wait for active readers.
if r != 0 && atomic.AddInt32(&rw.readerWait, r) != 0 {
runtime_SemacquireMutex(&rw.writerSem, false, 0)//true的话,放到队列最前面
}
if race.Enabled {
race.Enable()
race.Acquire(unsafe.Pointer(&rw.readerSem))
race.Acquire(unsafe.Pointer(&rw.writerSem))
}
}
func (rw *RWMutex) Unlock() {
if race.Enabled {
_ = rw.w.state
race.Release(unsafe.Pointer(&rw.readerSem))
race.Disable()
}
// Announce to readers there is no active writer.
r := atomic.AddInt32(&rw.readerCount, rwmutexMaxReaders)
if r >= rwmutexMaxReaders {
race.Enable()
fatal("sync: Unlock of unlocked RWMutex")
}
// Unblock blocked readers, if any.
for i := 0; i < int(r); i++ {
runtime_Semrelease(&rw.readerSem, false, 0)
}
// Allow other writers to proceed.
rw.w.Unlock()
if race.Enabled {
race.Enable()
}
}
func (rw *RWMutex) RLock() {
if race.Enabled {
_ = rw.w.state
race.Disable()
}
if atomic.AddInt32(&rw.readerCount, 1) < 0 {
// A writer is pending, wait for it.
runtime_SemacquireMutex(&rw.readerSem, false, 0)
}
if race.Enabled {
race.Enable()
race.Acquire(unsafe.Pointer(&rw.readerSem))
}
}
func (rw *RWMutex) RUnlock() {
if race.Enabled {
_ = rw.w.state
race.ReleaseMerge(unsafe.Pointer(&rw.writerSem))
race.Disable()
}
if r := atomic.AddInt32(&rw.readerCount, -1); r < 0 {
// Outlined slow-path to allow the fast-path to be inlined
rw.rUnlockSlow(r)//Mutex 的STATE - 1
}
if race.Enabled {
race.Enable()
}
}
Lock方法将rw锁定为写入状态,禁止其他线程读取或者写入。
Unlock方法解除rw的写入锁状态,如果m未加写入锁会导致运行时错误。
RLock方法将rw锁定为读取状态,禁止其他线程写入,但不禁止读取。
RUnlock方法解除rw的读取锁状态,如果m未加读取锁会导致运行时错误。
扩展
当写锁阻塞时,新的读锁是无法申请的。
即在 sync.RWMutex 的使用中,一个线程请求了他的写锁(mx.Lock())后,即便它还没有取到该锁(可能由于资源已被其他人锁定),后面所有的读锁的申请,都将被阻塞,只有取写锁的请求得到了锁且用完释放后,读锁才能去取。
如果一个线程因为某种原因,导致得不到CPU运行时间,这种状态被称之为 饥饿。当写锁阻塞时,新的读锁是无法申请的,这可以有效防止写者饥饿。然而,这种机制也禁止了读锁嵌套。读锁嵌套可能造成死锁。
例子
package main
import (
"fmt"
"sync"
"time"
)
func main() {
rw := new(sync.RWMutex)
var deadLockCase time.Duration = 1
go func() {
time.Sleep(time.Second * deadLockCase)
fmt.Println("Writer Try")
rw.Lock()
fmt.Println("Writer Fetch")
time.Sleep(time.Second * 1)
fmt.Println("Writer Release")
rw.Unlock()
}()
fmt.Println("Reader 1 Try")
rw.RLock()
fmt.Println("Reader 1 Fetch")
time.Sleep(time.Second * 2)
fmt.Println("Reader 2 Try")
rw.RLock()
fmt.Println("Reader 2 Fetch")
time.Sleep(time.Second * 2)
fmt.Println("Reader 1 Release")
rw.RUnlock()
time.Sleep(time.Second * 1)
fmt.Println("Reader 2 Release")
rw.RUnlock()
time.Sleep(time.Second * 2)
fmt.Println("Done")
}
运行结果
分析