发布时间:2024-12-23 03:57:02
在Go语言的并发编程中,锁起到了非常重要的作用。通过加锁可以保证在多个协程同时修改或访问共享资源时的数据一致性和线程安全性。Go语言提供了多种锁类型,其中最常用的就是互斥锁(sync.Mutex)和读写锁(sync.RWMutex)。本文将深入源码解析Go语言中锁的实现原理。
sync.Mutex 是 Go语言中最基本的互斥锁。它的定义如下:
type Mutex struct { state int32 // 锁的状态,0表示未加锁,1表示已加锁 sema uint32 // 等待获取锁的等待者数量 }
在使用 sync.Mutex 时,主要有两个方法:
除了互斥锁,Go语言还提供了读写锁 sync.RWMutex,它在 Mutex 的基础之上增加了读者计数器和写者计数器。其定义如下:
type RWMutex struct { w Mutex // 互斥锁,用于保护读写锁的状态字段和读写计数器 readerSem uint32 // 等待读者释放锁的等待者数量 writerSem uint32 // 等待写者释放锁的等待者数量 readerCount int32 // 当前持有读锁的读者数量 readerWait int32 // 等待获取读锁的读者数量 writerCount int32 // 当前持有写锁的写者数量 writerWait int32 // 等待获取写锁的写者数量 }
sync.RWMutex 提供了两种类型的锁定:读锁和写锁。
互斥锁和读写锁的实现都使用了底层的信号量(sync.semaphore)进行控制。当协程请求获取锁时,如果锁已经被其他协程持有,则会将当前协程阻塞并加入等待队列中,直到锁被释放后再唤醒协程。锁的唤醒机制使用了信号量的 P 操作和 V 操作。
为了避免因代码过长而影响读者阅读,我们只关注 Mutex 中的 Lock() 和 Unlock() 方法的源码实现。
func (m *Mutex) Lock() { if atomic.CompareAndSwapInt32(&m.state, 0, 1) { return } runtime_SemacquireMutex(&m.sema, false, 1) } func (m *Mutex) Unlock() { if atomic.CompareAndSwapInt32(&m.state, 1, 0) { runtime_SemreleaseMutex(&m.sema, 1) return } throw("sync: unlock of unlocked mutex") }
与互斥锁相比,读写锁的源码实现更为复杂。下面是读写锁中的 RLock() 和 Lock() 方法的源码:
func (rw *RWMutex) RLock() { if race.Enabled { _ = rw.w.state race.Disable() } // 尝试获取读锁 if atomic.AddInt32(&rw.readerCount, 1) < 0 { // 已经有协程持有写锁了,需要等待 runtime_SemacquireMutex(&rw.readerSem, false, 0) } if race.Enabled { race.Enable() race.Acquire(unsafe.Pointer(&rw.readerSem)) } } func (rw *RWMutex) Lock() { // 尝试获取写锁 if atomic.AddInt32(&rw.writerCount, -1) < 0 { // 已经有协程持有读锁或写锁了,需要等待 runtime_SemacquireMutex(&rw.writerSem, false, 0) } }
通过以上源码分析可以看出,读锁和写锁的实现都是通过底层信号量的 P 操作来阻塞协程。在获取读锁时,如果已经有协程持有写锁,则会阻塞以等待写锁释放;而在获取写锁时,如果有协程持有读锁或写锁,则会被阻塞等待。
本文对Go语言中互斥锁和读写锁的实现原理进行了分析和介绍。通过深入研究锁的源码,我们可以更好地理解锁的工作原理和使用方式,从而编写出更优秀的并发程序。