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的队尾。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值