Go并发编程(1)-Mutex源码实现
Mutex:如何解决资源并发访问问题
多线程访问共享资源,通过互斥锁来实现,其根本是对共享内存的锁定?(如果是多进程,还能简单使用互斥锁吗,是否需要分布式锁?)
同步原语的使用场景:
- 共享资源。并发地读写共享资源,会出现数据竞争(data race)的问题,所以需要 Mutex、RWMutex 这样的并发原语来保护。
- 任务编排。需要 goroutine 按照一定的规律执行,而 goroutine 之间有相互等待或者依赖的顺序关系,我们常常使用 WaitGroup 或者 Channel 来实现。
- 消息传递。信息交流以及不同的 goroutine 之间的线程安全的数据交流,常常使用 Channel 来实现。
Mutex:具体实现
第一版实现,使用CAS机制每次加锁时key值加一,每次释放时key值减一,但释放锁不会对groutine检查,允许任何groutine释放锁,
这个特性也一直保留至今。释放时如果有groutine在等待,则直接唤醒它让其获得锁。也就是说,新来的groutine先获得锁。
// CAS操作,当时还没有抽象出atomic包
func cas(val *int32, old, new int32) bool
func semacquire(*int32)
func semrelease(*int32)
// 互斥锁的结构,包含两个字段
type Mutex struct {
key int32 // 锁是否被持有的标识
sema int32 // 信号量专用,用以阻塞/唤醒goroutine
}
// 保证成功在val上增加delta的值
func xadd(val *int32, delta int32) (new int32) {
for {
v := *val
if cas(val, v, v+delta) {
return v + delta
}
}
panic("unreached")
}
// 请求锁
func (m *Mutex) Lock() {
if xadd(&m.key, 1) == 1 { //标识加1,如果等于1,成功获取到锁
return
}
semacquire(&m.sema) // 否则阻塞等待
}
func (m *Mutex) Unlock() {
if xadd(&m.key, -1) == 0 { // 将标识减去1,如果等于0,则没有其它等待者
return
}
semrelease(&m.sema) // 唤醒其它阻塞的goroutine
}
其中,xadd()函数内部利用CAS操作保证了操作原子性,当多个groutine同时调用Lock()函数时,不会出现某一瞬间key值被重复写,每次Lock()调用key值加一,如果加完后是1,说明只有一个groutine在获取锁,Lock()函数可以直接返回,代表加锁成功;当不等于1,说明此时有其他线程已经获得锁,要阻塞等待它将锁释放,因此xadd()结束后,利用信号量将此groutine放入一个FIFO队列。等到释放锁时,如果key值减一后不为0,说明有其他groutine也在尝试获取锁且阻塞等待,此时利用信号量唤醒等待的FIFO队列的队首groutine,那么这个队首groutine就从semacquire()处被唤醒,它的Lock()函数就可以返回,代表它获得锁成功。
因此,这种简单情况,就是所有同时请求锁的groutine放入一个FIFO队列,当释放锁时,按顺序让队首的groutine去获得锁。
注意的是,其中给state赋值的操作要采用CAS原子操作,并发时如两个groutine都操作cas(m.state, 0, 1)则只有一个会成功,另一个会失败,因此cas操作要写在for循环中。
第二版实现,为了给新来的groutine机会,在释放锁时,不再指定为FIFO队列中的首个获得锁,而是让它和新来的grountine进行竞争,
这样新来的就有机会获得锁。
type Mutex struct {
state int32
sema uint32
}
const (
mutexLocked = 1 << iota // mutex is locked, 1左移0位,即最低位1
mutexWoken // 即 mutexWoken = 1 << iota, 即mutexWoken = 1 << 1,1左移1位,即次低位1
mutexWaiterShift = iota // 即 mutexWoken = 2
)
func (m *Mutex) Lock() {
// Fast path: 幸运case,能够直接获取到锁
if atomic.CompareAndSwapInt32(&m.state, 0, mutexLocked) {
return
}
awoke := false
for {
old := m.state
new := old | mutexLocked // 新状态加锁
if old&mutexLocked != 0 {
new = old + 1<<mutexWaiterShift //等待者数量加一
}
if awoke {
// goroutine是被唤醒的,
// 新状态清除唤醒标志
new &^= mutexWoken
}
if atomic.CompareAndSwapInt32(&m.state, old, new) {//设置新状态
if old&mutexLocked == 0 { // 锁原状态未加锁
break
}
runtime.Semacquire(&m.sema) // 请求信号量
awoke = true
}
}
}
func (m *Mutex) Unlock() {
// Fast path: drop lock bit.
new := atomic.AddInt32(&m.state, -mutexLocked) //去掉锁标志
if (new+mutexLocked)&mutexLocked == 0 { //本来就没有加锁
panic("sync: unlock of unlocked mutex")
}
old := new
for {
if old>>mutexWaiterShift == 0 || old&(mutexLocked|mutexWoken) != 0 { // 没有等待者,或者有唤醒的waiter,或者锁原来已加锁
return
}
new = (old - 1<<mutexWaiterShift) | mutexWoken // 新状态,准备唤醒goroutine,并设置唤醒标志
if atomic.CompareAndSwapInt32(&m.state, old, new) {
runtime.Semrelease(&m.sema)
return
}
old = m.state
}
}
这一版的基本实现,仍然是Lock()调用时给stat赋值,但不是简单的数字加1操作,变为自定义的有特殊表示意义的值;同时赋值结束后,同第一版赋值成功后变为1则表明原本未加锁则函数返回不同,这里显式的判断赋值前未加锁,则赋值成功直接返回表示获取锁成功;再者,第一版睡眠函数后没有执行语句,表明唤醒后函数返回获取到了锁,而这里唤醒后进入下一次循环,并没有直接返回,也就没有直接获得锁,如果在下一次循环中,有其他新的groutine执行Lock,那么就产生了竞争,看哪个加锁成功,这也就是“给新人机会”。
这其中几个注意的点,<1> state最低位表示锁位,如果为1时有Lock()操作,则Lock()中cas执行成功后,肯定会陷入睡眠,就和第一版中cas后state不为1则睡眠机制相同。<2>次低位表示唤醒位,这一位表示有陷入睡眠的线程被唤醒来争抢锁,只会在Unlock()时由执行该Unlock()的groutine在一定条件下进行设置,设置为1后才会唤醒某个groutine。<3>其余高位表示等待获得锁的groutine数量,它会在有groutine进行Lock()时做增加操作,Unlock()唤醒groutine时做减少操作。
将status字段按比特位标记含义,最低位mutexLocked表示当前Mutex是否被锁;次低位mutexWoken表示表示当前是否有groutine被唤醒,有则代表该被唤醒的groutine会去尝试获取锁;剩余的高位表示当前等待获得锁的线程数。
基本思路是:多个groutine同时调用Lock()即同时获得锁时,如果当前锁未被任何人持有,则调用CompareAndSwapInt32()原子操作(将初始值设为1,即mutexLocked位为1)成功的groutine,即代表获取锁成功,可以不阻塞不等待直接返回。
否则,根据当前state字段,进行后续的设置new state值,并判断是否需要休眠等待。
我自己写的第二版大致结构:
package mymutex
type Mutex struct {
state int32
sema uint32
}
const (
mutexLocked = 1 << iota
mutexWoken
mutexWaiterShift = iota
)
func CompareAndSwapInt32(state *int32, value int32, newValue int32) bool {
}
func Wait(sema *uint32) {
}
func (m *Mutex) Lock() {
if CompareAndSwapInt32(&m.state, 0, mutexLocked) {
return
}
// 基本流程,设置新值,并判断是否直接返回或阻塞等待
wakeFlag := false
for {
// 在加锁时末尾可能为1,可能为0
old := m.state
new := old | mutexLocked
if old |mutexLocked != 0 {
new += 1 << mutexWaiterShift
}
if !wakeFlag {
new &^= mutexWoken // 唤醒去掉唤醒标志
}
if CompareAndSwapInt32(&m.state, old, new) {
if old |mutexLocked == 0 {
return
}else {
Wait(&m.sema) // 这里唤醒后不让return
//return
wakeFlag = true
}
}
}
}
func atomic_add(m *int32, delta int32) int32 {
for{
old := *m
new := old + delta
if CompareAndSwapInt32(m, old, new) {
return new
}
}
}
func wakeup() {
}
func (m *Mutex) Unlock() {
//old := m.state
//if old & mutexLocked == 0 { // 但其实这么写是有问题的,因此取数和判断不是原子操作,或者这里判断后立马另一线程又改变了state,其实有可以Unlock了
// // 所以先执行原子赋值操作
//}
new := atomic_add(&m.state, -mutexLocked)
if (new +mutexLocked) &mutexLocked == 0 { //判断本次赋值是否合法
panic("error")
}
old := new
// 刚减一后,需要将得到的值判断是否需要做唤醒操作,唤醒成功,说明此时没有人在同时修改m.state的值
// 失败,则表明当前state值已被修改,谁会进行修改?
// 修改只会发生在Lock和Unlock,如果是Lock,那么此时该UnLock还未唤醒,肯定是新来的groutine进行了Lock
// 如果是另外groutine的UnLock,则由于此时锁位为0,肯定会报错失败
// 这里可以看到,失败只会是由新来的lock触发的,那么失败后,是否就可以直接返回了,毕竟新的lock已经锁上了,这个lock肯定也释放了
// 但是这里严谨一点,在一下for循环中,判断当前state的锁位是否为1, 为1 则退出
// 上述漏掉了一点,失败还有可能是中间status被lock加一又被lock减一,此时两个Unlock函数都尝试做cas操作,则有一个会失败
for {
// 这时候就要判断是直接返回还是要唤醒,是old值还是判断m.state?
// old是本线程的局部变量,虽然它代表这更新后的state值,但判断逻辑肯定要用old,m.state是不断变化的
// 减一操作后,等待数量为0则返回
if old >>mutexWaiterShift == 0 || old&(mutexWoken|mutexLocked) != 0{
return
}
// 需要唤醒1个groutine
new = old - 1 <<mutexWaiterShift
new |= mutexWoken
if CompareAndSwapInt32(&m.state, old, new) {
wakeup()
break
}
old = m.state
}
}
第三版实现,为了优先让正处于cpu运行中的groutine获得锁(这样可以减少groutine切换的上下文),在groutine尝试获得锁时,让其自旋一定次数(自旋时一直占用cpu),
如果期间锁被其他groutine释放了,那么它会优先获得锁,这样性能会更好。也就是“多给新人机会”,大部分都是新来的groutine获得锁。
func (m *Mutex) Lock() {
// Fast path: 幸运之路,正好获取到锁
if atomic.CompareAndSwapInt32(&m.state, 0, mutexLocked) {
return
}
awoke := false
iter := 0
for { // 不管是新来的请求锁的goroutine, 还是被唤醒的goroutine,都不断尝试请求锁
old := m.state // 先保存当前锁的状态
new := old | mutexLocked // 新状态设置加锁标志
if old&mutexLocked != 0 { // 锁还没被释放
if runtime_canSpin(iter) { // 还可以自旋
if !awoke && old&mutexWoken == 0 && old>>mutexWaiterShift != 0 &&
atomic.CompareAndSwapInt32(&m.state, old, old|mutexWoken) {
awoke = true
}
runtime_doSpin()
iter++
continue // 自旋,再次尝试请求锁
}
new = old + 1<<mutexWaiterShift
}
if awoke { // 唤醒状态
if new&mutexWoken == 0 {
panic("sync: inconsistent mutex state")
}
new &^= mutexWoken // 新状态清除唤醒标记
}
if atomic.CompareAndSwapInt32(&m.state, old, new) {
if old&mutexLocked == 0 { // 旧状态锁已释放,新状态成功持有了锁,直接返回
break
}
runtime_Semacquire(&m.sema) // 阻塞等待
awoke = true // 被唤醒
iter = 0
}
}
}
主要思路:当执行Lock()函数获取锁时,如果发现lock位为1,则将woken未置为1,然后自旋,一直检测能否lock位是否变为0.这样如果在自旋期间有其他groutine释放了锁,释放后不会执行唤醒,因为woken位已经是1,那么将会由此自旋的groutine获取锁。因此如果临界区小的话,自旋会避免了cpu上的线程切换,总体上性能更高。
第四版实现,为了防止第三版中睡眠的groutine一直获取不到锁,给其陷入睡眠时间做一个时间上限1ms,超过这个时间则锁结构进入饥饿模式,
饥饿模式下,锁被分配给FIFO队列的首个groutine,新来的groutine不自旋也不枪锁,直接放入FIFO的队尾。
type Mutex struct {
state int32
sema uint32
}
const (
mutexLocked = 1 << iota // mutex is locked
mutexWoken
mutexStarving // 从state字段中分出一个饥饿标记
mutexWaiterShift = iota
starvationThresholdNs = 1e6
)
func (m *Mutex) Lock() {
// Fast path: 幸运之路,一下就获取到了锁
if atomic.CompareAndSwapInt32(&m.state, 0, mutexLocked) {
return
}
// Slow path:缓慢之路,尝试自旋竞争或饥饿状态下饥饿goroutine竞争
m.lockSlow()
}
func (m *Mutex) lockSlow() {
var waitStartTime int64
starving := false // 此goroutine的饥饿标记
awoke := false // 唤醒标记
iter := 0 // 自旋次数
old := m.state // 当前的锁的状态
for {
// 锁是非饥饿状态,锁还没被释放,尝试自旋
if old&(mutexLocked|mutexStarving) == mutexLocked && runtime_canSpin(iter) {
if !awoke && old&mutexWoken == 0 && old>>mutexWaiterShift != 0 &&
atomic.CompareAndSwapInt32(&m.state, old, old|mutexWoken) {
awoke = true
}
runtime_doSpin()
iter++
old = m.state // 再次获取锁的状态,之后会检查是否锁被释放了
continue
}
new := old
if old&mutexStarving == 0 {
new |= mutexLocked // 非饥饿状态,加锁
}
if old&(mutexLocked|mutexStarving) != 0 {
new += 1 << mutexWaiterShift // waiter数量加1
}
if starving && old&mutexLocked != 0 {
new |= mutexStarving // 设置饥饿状态
}
if awoke {
if new&mutexWoken == 0 {
throw("sync: inconsistent mutex state")
}
new &^= mutexWoken // 新状态清除唤醒标记
}
// 成功设置新状态
if atomic.CompareAndSwapInt32(&m.state, old, new) {
// 原来锁的状态已释放,并且不是饥饿状态,正常请求到了锁,返回
if old&(mutexLocked|mutexStarving) == 0 {
break // locked the mutex with CAS
}
// 处理饥饿状态
// 如果以前就在队列里面,加入到队列头
queueLifo := waitStartTime != 0
if waitStartTime == 0 {
waitStartTime = runtime_nanotime()
}
// 阻塞等待
runtime_SemacquireMutex(&m.sema, queueLifo, 1)
// 唤醒之后检查锁是否应该处于饥饿状态
starving = starving || runtime_nanotime()-waitStartTime > starvationThresholdNs
old = m.state
// 如果锁已经处于饥饿状态,直接抢到锁,返回
if old&mutexStarving != 0 {
if old&(mutexLocked|mutexWoken) != 0 || old>>mutexWaiterShift == 0 {
throw("sync: inconsistent mutex state")
}
// 有点绕,加锁并且将waiter数减1
delta := int32(mutexLocked - 1<<mutexWaiterShift)
if !starving || old>>mutexWaiterShift == 1 {
delta -= mutexStarving // 最后一个waiter或者已经不饥饿了,清除饥饿标记
}
atomic.AddInt32(&m.state, delta)
break
}
awoke = true
iter = 0
} else {
old = m.state
}
}
}
func (m *Mutex) Unlock() {
// Fast path: drop lock bit.
new := atomic.AddInt32(&m.state, -mutexLocked)
if new != 0 {
m.unlockSlow(new)
}
}
func (m *Mutex) unlockSlow(new int32) {
if (new+mutexLocked)&mutexLocked == 0 {
throw("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<<mutexWaiterShift) | mutexWoken
if atomic.CompareAndSwapInt32(&m.state, old, new) {
runtime_Semrelease(&m.sema, false, 1)
return
}
old = m.state
}
} else {
runtime_Semrelease(&m.sema, true, 1)
}
}
或者
func (m *Mutex) Lock() {
// 如果mutext的state没有被锁,也没有等待/唤醒的goroutine, 锁处于正常状态,那么获得锁,返回.
// 比如锁第一次被goroutine请求时,就是这种状态。或者锁处于空闲的时候,也是这种状态。
if atomic.CompareAndSwapInt32(&m.state, 0, mutexLocked) {
return
}
// 标记本goroutine的等待时间
var waitStartTime int64
// 本goroutine是否已经处于饥饿状态
starving := false
// 本goroutine是否已唤醒
awoke := false
// 自旋次数
iter := 0
// 复制锁的当前状态
old := m.state
for {
// 第一个条件是state已被锁,但是不是饥饿状态。如果时饥饿状态,自旋时没有用的,锁的拥有权直接交给了等待队列的第一个。
// 第二个条件是还可以自旋,多核、压力不大并且在一定次数内可以自旋, 具体的条件可以参考`sync_runtime_canSpin`的实现。
// 如果满足这两个条件,不断自旋来等待锁被释放、或者进入饥饿状态、或者不能再自旋。
if old&(mutexLocked|mutexStarving) == mutexLocked && runtime_canSpin(iter) {
// 自旋的过程中如果发现state还没有设置woken标识,则设置它的woken标识, 并标记自己为被唤醒。
if !awoke && old&mutexWoken == 0 && old>>mutexWaiterShift != 0 &&
atomic.CompareAndSwapInt32(&m.state, old, old|mutexWoken) {
awoke = true
}
runtime_doSpin()
iter++
old = m.state
continue
}
// 到了这一步, state的状态可能是:
// 1. 锁还没有被释放,锁处于正常状态
// 2. 锁还没有被释放, 锁处于饥饿状态
// 3. 锁已经被释放, 锁处于正常状态
// 4. 锁已经被释放, 锁处于饥饿状态
//
// 并且本gorutine的 awoke可能是true, 也可能是false (其它goutine已经设置了state的woken标识)
// new 复制 state的当前状态, 用来设置新的状态
// old 是锁当前的状态
new := old
// 如果old state状态不是饥饿状态, new state 设置锁, 尝试通过CAS获取锁,
// 如果old state状态是饥饿状态, 则不设置new state的锁,因为饥饿状态下锁直接转给等待队列的第一个.
if old&mutexStarving == 0 {
new |= mutexLocked
}
// 将等待队列的等待者的数量加1
if old&(mutexLocked|mutexStarving) != 0 {
new += 1 << mutexWaiterShift
}
// 如果当前goroutine已经处于饥饿状态, 并且old state的已被加锁,
// 将new state的状态标记为饥饿状态, 将锁转变为饥饿状态.
if starving && old&mutexLocked != 0 {
new |= mutexStarving
}
// 如果本goroutine已经设置为唤醒状态, 需要清除new state的唤醒标记, 因为本goroutine要么获得了锁,要么进入休眠,
// 总之state的新状态不再是woken状态.
if awoke {
if new&mutexWoken == 0 {
throw("sync: inconsistent mutex state")
}
new &^= mutexWoken
}
// 通过CAS设置new state值.
// 注意new的锁标记不一定是true, 也可能只是标记一下锁的state是饥饿状态.
if atomic.CompareAndSwapInt32(&m.state, old, new) {
// 如果old state的状态是未被锁状态,并且锁不处于饥饿状态,
// 那么当前goroutine已经获取了锁的拥有权,返回
if old&(mutexLocked|mutexStarving) == 0 {
break
}
// 设置/计算本goroutine的等待时间
queueLifo := waitStartTime != 0
if waitStartTime == 0 {
waitStartTime = runtime_nanotime()
}
// 既然未能获取到锁, 那么就使用sleep原语阻塞本goroutine
// 如果是新来的goroutine,queueLifo=false, 加入到等待队列的尾部,耐心等待
// 如果是唤醒的goroutine, queueLifo=true, 加入到等待队列的头部
runtime_SemacquireMutex(&m.sema, queueLifo)
// sleep之后,此goroutine被唤醒
// 计算当前goroutine是否已经处于饥饿状态.
starving = starving || runtime_nanotime()-waitStartTime > starvationThresholdNs
// 得到当前的锁状态
old = m.state
// 如果当前的state已经是饥饿状态
// 那么锁应该处于Unlock状态,那么应该是锁被直接交给了本goroutine
if old&mutexStarving != 0 {
// 如果当前的state已被锁,或者已标记为唤醒, 或者等待的队列中不为空,
// 那么state是一个非法状态
if old&(mutexLocked|mutexWoken) != 0 || old>>mutexWaiterShift == 0 {
throw("sync: inconsistent mutex state")
}
// 当前goroutine用来设置锁,并将等待的goroutine数减1.
delta := int32(mutexLocked - 1<<mutexWaiterShift)
// 如果本goroutine是最后一个等待者,或者它并不处于饥饿状态,
// 那么我们需要把锁的state状态设置为正常模式.
if !starving || old>>mutexWaiterShift == 1 {
// 退出饥饿模式
delta -= mutexStarving
}
// 设置新state, 因为已经获得了锁,退出、返回
atomic.AddInt32(&m.state, delta)
break
}
// 如果当前的锁是正常模式,本goroutine被唤醒,自旋次数清零,从for循环开始处重新开始
awoke = true
iter = 0
} else { // 如果CAS不成功,重新获取锁的state, 从for循环开始处重新开始
old = m.state
}
}
}
func (m *Mutex) Unlock() {
// 如果state不是处于锁的状态, 那么就是Unlock根本没有加锁的mutex, panic
new := atomic.AddInt32(&m.state, -mutexLocked)
if (new+mutexLocked)&mutexLocked == 0 {
throw("sync: unlock of unlocked mutex")
}
// 释放了锁,还得需要通知其它等待者
// 锁如果处于饥饿状态,直接交给等待队列的第一个, 唤醒它,让它去获取锁
// 锁如果处于正常状态,
// new state如果是正常状态
if new&mutexStarving == 0 {
old := new
for {
// 如果没有等待的goroutine, 或者锁不处于空闲的状态,直接返回.
if old>>mutexWaiterShift == 0 || old&(mutexLocked|mutexWoken|mutexStarving) != 0 {
return
}
// 将等待的goroutine数减一,并设置woken标识
new = (old - 1<<mutexWaiterShift) | mutexWoken
// 设置新的state, 这里通过信号量会唤醒一个阻塞的goroutine去获取锁.
if atomic.CompareAndSwapInt32(&m.state, old, new) {
runtime_Semrelease(&m.sema, false)
return
}
old = m.state
}
} else {
// 饥饿模式下, 直接将锁的拥有权传给等待队列中的第一个.
// 注意此时state的mutexLocked还没有加锁,唤醒的goroutine会设置它。
// 在此期间,如果有新的goroutine来请求锁, 因为mutex处于饥饿状态, mutex还是被认为处于锁状态,
// 新来的goroutine不会把锁抢过去.
runtime_Semrelease(&m.sema, true)
}
}
大概思路:线程调用Lock()锁时,
使用Mutex常见的 4 种错误场景:分别是 Lock/Unlock 不是成对出现、Copy 已使用的 Mutex、重入和死锁
自身理解
自旋
只会发生在锁还没释放的时候,它的目的是在临界去很小时,释放锁后立即被自旋groutine获得锁
而原来“给新人机会”的第二个版本,
unlock完第一步就有新来的,它检测到locked位为0, 可能会抢锁成功,将locked位置为1,这是“给新人机会”
“多给新人机会”指自旋,在未释放时有请求加锁,其实先不让它睡眠,而是自旋,可能自旋期间能获得锁,同已睡眠的相比,
它就是新人,并且是在locked位为1时的新人,因此可以称之为“多给新人机会”。如果此新人和更新的新人共同争锁呢?
如果再释放锁时一直有自旋的groutine,那么是否就不会执行唤醒操作了呢?
饥饿模式下的解锁,肯定是有waiter的,通知这个等待者,然后它获得锁。
那么在设置饥饿模式时,,肯定是当前有人在持有锁,才会设置才饥饿模式。
第一版实现,使用CAS机制每次加锁时key值加一,每次释放时key值减一,但释放锁不会对groutine检查,允许任何groutine释放锁,
这个特性也一直保留至今。释放时如果有groutine在等待,则直接唤醒它让其获得锁。也就是说,新来的groutine先获得锁。
第二版实现,为了给新来的groutine机会,在释放锁时,不再指定为FIFO队列中的首个获得锁,而是让它和新来的grountine进行竞争,
这样新来的就有机会获得锁。
第三版实现,为了优先让正处于cpu运行中的groutine获得锁(这样可以减少groutine切换的上下文),在groutine尝试获得锁时,让其自旋一定次数(自旋时一直占用cpu),
如果期间锁被其他groutine释放了,那么它会优先获得锁,这样性能会更好。也就是“多给新人机会”,大部分都是新来的groutine获得锁。
第四版实现,为了防止第三版中睡眠的groutine一直获取不到锁,给其陷入睡眠时间做一个时间上限1ms,超过这个时间则锁结构进入饥饿模式,
饥饿模式下,锁被分配给FIFO队列的首个groutine,新来的groutine不自旋也不枪锁,直接放入FIFO的队尾。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· 没有Manus邀请码?试试免邀请码的MGX或者开源的OpenManus吧
· 【自荐】一款简洁、开源的在线白板工具 Drawnix
· 园子的第一款AI主题卫衣上架——"HELLO! HOW CAN I ASSIST YOU TODAY