荡俊屯 发表于 2025-6-4 22:48:28

golang单机锁实现

1、锁的概念引入

首先,为什么需要锁?
在并发编程中,多个线程或进程可能同时访问和修改同一个共享资源(例如变量、数据结构、文件)等,若不引入合适的同步机制,会引发以下问题:

[*]数据竞争:多个线程同时修改一个资源,最终的结果跟线程的执行顺序有关,结果是不可预测的。
[*]数据不一致:一个线程在修改资源,而另一个线程读取了未修改完的数据,从而导致读取了错误的数据。
[*]资源竞争:多线程竞争同一个资源,浪费系统的性能。
因此,我们需要一把锁,来保证同一时间只有一个人能写数据,确保共享资源在并发访问下的正确性和一致性。
在这里,引入两种常见的并发控制处理机制,即乐观锁与悲观锁:

[*]乐观锁:假定在并发操作中,资源的抢占并不是很激烈,数据被修改的可能性不是很大,那这时候就不需要对共享资源区进行加锁再操作,而是先修改了数据,最终来判断数据有没有被修改,没有被修改则提交修改指,否则重试。
[*]悲观锁:与乐观锁相反,它假设场景的资源竞争激烈,对共享资源区的访问必须要求持有锁。
针对不同的场景需要采取因地制宜的策略,比较乐观锁与悲观所,它们的优缺点显而易见:
策略优点缺点乐观锁不需要实际上锁,性能高若冲突时,需要重新进行操作,多次重试可能会导致性能下降明显悲观锁访问数据一定需要持有锁,保证并发场景下的数据正确性加锁期间,其他等待锁的线程需要被阻塞,性能低2、Sync.Mutex

Go对单机锁的实现,考虑了实际环境中协程对资源竞争程度的变化,制定了一套锁升级的过程。具体方案如下:

[*]首先采取乐观的态度,Goroutine会保持自旋态,通过CAS操作尝试获取锁。
[*]当多次获取失败,将会由乐观态度转入悲观态度,判定当前并发资源竞争程度剧烈,进入阻塞态等待被唤醒。
从乐观转向悲观的判定规则如下,满足其中之一即发生转变:

[*]Goroutine自旋尝试次数超过4次
[*]当前P的执行队列中存在等待被执行的G(避免自旋影响GMP调度性能)
[*]CPU是单核的(其他Goroutine执行不了,自旋无意义)
除此之外,为了防止被阻塞的协程等待过长时间也没有获取到锁,导致用户的整体体验下降,引入了饥饿的概念:

[*]饥饿态:若Goroutine被阻塞等待的时间>1ms,则这个协程被视为处于饥饿状态
[*]饥饿模式:表示当前锁是否处于特定的模式,在该模式下,锁的交接是公平的,按顺序交给等待最久的协程。
饥饿模式与正常模式的转变规则如下:

[*]普通模式->饥饿模式:存在阻塞的协程,阻塞时间超过1ms
[*]饥饿模式->普通模式:阻塞队列清空,亦或者获得锁的协程的等待时间小于1ms,则恢复
接下来步入源码,观看具体的实现。
2.1、数据结构

位于包sync/mutex.go中,对锁的定义如下:
type Mutex struct {
        state int32
        semauint32
}

[*]state:标识目前锁的状态信息,包括了是否处于饥饿模式、是否存在唤醒的阻塞协程、是否上锁、以及处于等待锁的协程个数有多少。
[*]seme:用于阻塞和唤醒协程的信号量。
将state看作一个二进制字符串,它存储信息的规则如下:

[*]第一位标识是否处于上锁,0表示否,1表示上锁(mutexLocked)
[*]第二位标识是否存在唤醒的阻塞协程(mutexWoken)
[*]第三位标识是否处于饥饿模式(mutexStarving)
[*]从第四位开始,记录了处于阻塞态的协程个数
const (
        mutexLocked = 1 << iota // mutex is locked
        mutexWoken
        mutexStarving
        mutexWaiterShift = iota
        starvationThresholdNs = 1e6 //饥饿阈值
)(2)进入尝试获取锁的循环中,两个if表示:
<ul>若锁处于上锁状态,并且不处于饥饿状态中,并且当前的协程允许继续自旋下去(非单核CPU、自旋次数>mutexWaiterShift == 0 {                                        throw("sync: inconsistent mutex state")                                }                //将要更新的信号量                                delta := int32(mutexLocked - 1mutexWaiterShift == 1 {                                        delta -= mutexStarving                                }                                atomic.AddInt32(&m.state, delta)                                break                        }                        awoke = true                        iter = 0      //....                } else {                        //...                }从阻塞中唤醒,首先计算一些协程的阻塞时间,以及当前的最新锁状态。
若锁处于饥饿模式:那么当前协程将直接获取锁,当前协程是因为饥饿模式被唤醒的,不存在其他协程抢占锁。于是更新信号量,将记录阻塞协程数-1,将锁的上锁态置1。若当前从饥饿模式唤醒的协程,等待时间已经不到1ms了或者是最后一个等待的协程,那么将将锁从饥饿模式转化为正常模式。至此,获取成功,退出函数。
否则,只是普通的随机唤醒,于是开始尝试进行抢占,回到步骤1。
2.4、释放锁Unlock()

func (m *Mutex) Lock() {
        if atomic.CompareAndSwapInt32(&m.state, 0, mutexLocked) {
                return
        }
        m.lockSlow()
}通过原子操作,直接将锁的mutexLocked标识置为0。若置0后,锁的状态不为0,那就说明存在需要获取锁的协程,步入unlockSlow。
2.5、unlockSlow()

func (m *Mutex) unlockSlow(new int32) {        if (new+mutexLocked)&mutexLocked == 0 {                fatal("sync: unlock of unlocked mutex")        }        if new&mutexStarving == 0 {                old := new                for {                        if old>>mutexWaiterShift == 0 || old&(mutexLocked|mutexWoken|mutexStarving) != 0 {                                return                        }                        new = (old - 1
页: [1]
查看完整版本: golang单机锁实现