乐趣区

关于golang:Go-的-sync-同步原语库

官网包的正文:

// Package sync provides basic synchronization primitives such as mutual
// exclusion locks. Other than the Once and WaitGroup types, most are intended
// for use by low-level library routines. Higher-level synchronization is
// better done via channels and communication.

sync包提供根底的同步原语,sync.Mutextsync.RWMutexsync.WaitGroupsync.Oncesync.Cond

一、Mutex

Go 语言的 sync.Mutex 由两个字段 statesema组成。其中,state示意以后互斥锁的状态,sema是用来管制锁状态的信号量。

type Mutex struct {
    state int32
    sema  uint32
}

上述两个加起来只占 8 字节空间的构造体表过了 Go 语言中的互斥锁。

1 状态

互斥锁的状态:

const (
    mutexLocked = 1 << iota // 锁定
    mutexWoken // 唤醒
    mutexStarving  // 饥饿
  ...
)

2 模式

sync.Mutex有两种模式——失常模式和饥饿模式。

在失常模式下,锁的期待者会依照先进先出的程序获取锁。然而刚被唤起的 Goroutine 与新创建的 Goroutine 竞争时,大概率会获取不到锁,为了缩小这种状况的呈现,一旦 Goroutine 超过 1ms 没有获取到锁,它就会将以后互斥锁切换为饥饿模式,避免局部 Goroutine 被“饿死”。

引入饥饿模式的目标是为了保障互斥锁的公平性。在饥饿模式中,互斥锁会间接交给期待队列最后面的 Goroutine。新的 Goroutine 在该状态下不能获取锁,也不会进入自旋状态,只会在队列的开端期待。如果一个 Goroutine 获取到了互斥锁并且它在队列开端的工夫或者它期待的工夫少于 1ms,那么以后的互斥锁就会切换回失常模式。

与饥饿模式相比,失常模式下的互斥锁可能提供更好的性能,饥饿模式能防止 Goroutine 因为陷入期待无奈获取锁而造成的高尾提早。

3 上锁

上锁sync.Mutex.Lock,解锁sync.Mutex.Unlock

互斥锁的上锁办法通过精简,办法的骨干只保留最常见、简略的状况 ——当锁的状态是 0 时,将 mutextLocked 地位换成 1:

func (m *Mutex) Lock() {
    // Fast path: grab unlocked mutex.
    if atomic.CompareAndSwapInt32(&m.state, 0, mutexLocked) {
        if race.Enabled {race.Acquire(unsafe.Pointer(m))
        }
        return
    }
    // Slow path (outlined so that the fast path can be inlined)
    m.lockSlow()}

如果互斥锁的状态不是 0 时就会调用 sync.Mutex.lockSlow 尝试通过自旋等形式期待锁的开释,该办法的主体是一个十分大的 for 循环。这里将它分成几个局部进行介绍:

  1. 判断以后 Goroutine 是否进入自旋
  2. 通过自旋期待互斥锁的开释
  3. 计算互斥锁的最新状态
  4. 更新互斥锁的状态并获取锁

3.1 判断 $G$ 是否自旋

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
        }
}

自旋是一种多线程同步机制,以后的过程在进入自旋的过程中会始终放弃 CPU 的占用,继续查看某个条件是否为真。在多核处理器上,自旋能够防止 $G$ 的切换,应用失当能更好地利用资源,施展更好的性能,然而使用不当,会拖慢整个程序,所以 $G$ 进入自旋的条件十分刻薄:

  1. 互斥锁只有在一般模式能力进入自旋(Don’t spin in starvation mode)
  2. runtime.sync_runtime_canSpin须要返回true

    1. 运行在多核处理器上
    2. 以后 $G$ 为了获取该锁进入自旋的次数小于四次
    3. 以后机器上至多存在一个正在运行的处理器 $P$ 并且其运行队列为空

3.2 自旋占用 CPU

一旦以后 $G$ 可能进入自旋就会调用 runtime.sync_runtime_doSpinruntime.procyield并执行 30 次的 PAUSE 指令,该指令只会占用 CPU 并耗费 CPU 工夫:

func sync_runtime_doSpin() {procyield(active_spin_cnt)
}

TEXT runtime·procyield(SB),NOSPLIT,$0-0
    MOVL    cycles+0(FP), AX
again:
    PAUSE
    SUBL    $1, AX
    JNZ    again
    RET

3.3 计算锁的状态

解决了自旋相干的非凡逻辑之后,互斥锁会依据上下文计算以后互斥锁最新的状态。几个不同的条件别离会更新 state 字段中存储的不同信息:

const (    
    mutexLocked
    mutexWoken
    mutexStarving
    mutexWaiterShift
)
new := old
if old&mutexStarving == 0 {new |= mutexLocked}
if old&(mutexLocked|mutexStarving) != 0 {new += 1 << mutexWaiterShift}
if starving && old&mutexLocked != 0 {new |= mutexStarving}
if awoke {
  if new&mutexWoken == 0 {throw("sync: inconsistent mutex state")
  }
  new &^= mutexWoken
}

3.4 更新锁状态

计算了新的互斥锁状态之后,会应用 CAS 函数 sync/atomic.CompareAndSwapInt32 更新状态:

if atomic.CompareAndSwapInt32(&m.state, old, new) {if old&(mutexLocked|mutexStarving) == 0 {break // 用 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")
    }
    delta := int32(mutexLocked - 1<<mutexWaiterShift)
    if !starving || old>>mutexWaiterShift == 1 {delta -= mutexStarving}
    atomic.AddInt32(&m.state, delta)
    break
  }
  awoke = true
  iter = 0
} else {old = m.state}

如果没有通过 CAS 取得锁,会调用 runtime.sync_runtime_SemacquireMutex 通过信号量保障资源不会被两个 $G$ 获取。runtime.sync_runtime_SemacquireMutex会在办法中一直尝试获取锁并陷入休眠期待信号量的开释,一旦以后 $G$ 能够获取信号量,它就会立即返回,sync.Mutex.Lock的残余代码也会继续执行。

  • 在失常模式下,这段代码会设置唤醒和饥饿标记、重置迭代次数并从新执行获取锁的循环
  • 在饥饿模式下,以后 $G$ 会取得互斥锁,如果期待队列中只存在以后 $G$,互斥锁还会从饥饿模式中退出

4 解锁

互斥锁的解锁过程 sync.Mutex.Unlock 与加锁过程相比就很简略,该过程会先应用 sync/atomic.AddInt32 函数疾速解锁,这时会产生上面的两种状况:

  1. 如果该函数返回的新状态等于 0,以后 $G$ 就胜利解锁了互斥锁
  2. 如果该函数返回的新状态不等于 0,这段代码会调用 sync.Mutex.unlockSlow 开始慢速解锁
func (m *Mutex) Unlock() {
    ...
    new := atomic.AddInt32(&m.state, -mutexLocked)
    if new != 0 {m.unlockSlow(new)
    }
}

sync.Mutex.unlockSlow会先校验锁状态的合法性——如果以后互斥锁曾经被解锁过了会间接抛出导常”sync: unlock of unlocked mutex“终止程序。

在失常状况下,会依据以后互斥锁的状态,别离解决失常模式和饥饿模式下的互斥锁:

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)
    }
}
  1. 在失常模式下,上述代码会应用如下所示的处理过程:

    • 如果互斥锁不存在期待者或互斥锁的 mutexLockedmutexStarvingmutexWoken 状态都不为 0,那么以后办法能够间接返回,不须要唤醒其余期待者
    • 如果互斥锁存在期待者,会通过 runtime.sync_runtime_Semrelease 唤醒期待者并移交锁的所有权
  2. 在饥饿模式下,上述代码会间接调用 runtime.sync_runtime_Semrelease 将以后锁交给下一个正在尝试获取锁的期待者,期待者被唤醒后会失去锁,在这时互斥锁还不会退出饥饿状态

5 小结

对上锁和解锁进行简略总结。

互斥锁的上锁过程比较复杂,波及自旋、信号量以及调度等概念:

  • 如果互斥锁解决初始化状态,会通过置位 mutexLocked 上锁
  • 如果互斥锁解决 mutexLocked 状态并且在一般模式下工作,会进入自旋,执行 30 次 PAUSE 指令占用 CPU 工夫期待锁的开释
  • 如果以后 $G$ 期待锁的工夫超过了 1ms,互斥锁就会切换到饥饿模式
  • 互斥锁在失常状况下会通过 runtime.sync_runtime_SemacquireMutex 将尝试获取锁的 $G$ 切换到休眠状态,期待锁的持有者唤醒
  • 如果以后 $G$ 是互斥锁上的最初一个期待的协程或者期待的工夫小于 1ms,那么它会将互斥锁切换回失常模式

互斥锁的解锁过程与之相比就比较简单,其代码行数不多、逻辑清晰,也比拟容易了解:

  • 当互斥锁曾经被解锁时,调用 sync.Mutex.Unlock 会间接抛出异样
  • 当互斥锁解决饥饿模式时,将锁的所有权交给队列中的下一个期待者,期待者会负责设置 mutexLocked 标记位
  • 当互斥锁解决一般模式时,如果没有 $G$ 期待锁的开释或者曾经有被唤醒的 $G$ 获取了锁,会间接返回;在其余状况下会通过 runtime.sync_runtime_Semrelease 唤醒对应的 $G$

二、RWMutex

读写互斥锁 sync.RWMutex 是细粒度的互斥锁,它不限度资源的并发读,然而读写、写写操作无奈并行执行。

常见服务的资源读写比例会十分高,因为大多数的读申请之间不会相互影响,所以咱们能够拆散读写操作,以此来进步服务的性能。

1 构造体

type RWMutex struct {w           Mutex  // 如果有未实现 (pending) 的写操作 (writers) 就始终维持互斥锁
    writerSem   uint32 // 写期待读的信号
    readerSem   uint32 // 读期待写的信号
    readerCount int32  // 未实现 (pending) 的读操作 (readers) 的数量
  readerWait  int32  // 行将完结 / 正在来到 (departing) 的读操作的数量
}

2 写锁

写操作的锁应用 sync.RWMutex.Locksync.RWMutex.Unlock办法。

当资源的使用者想要获取锁时,须要调用 sync.RWMutex.Lock 办法:

func (rw *RWMutex) Lock() {
    ...
    rw.w.Lock()
  // 通过把 rw.readerCount 设置为正数,来告知读操作所有者有写操作未实现
    r := atomic.AddInt32(&rw.readerCount, -rwmutexMaxReaders) + rwmutexMaxReaders
    if r != 0 && atomic.AddInt32(&rw.readerWait, r) != 0 {runtime_SemacquireMutex(&rw.writerSem, false, 0)
    }
    ...
}
  • 调用构造体持有的 sync.Mutex 构造体的 sync.Mutex.Lock 阻塞后续的写操作

    • 因为互斥锁曾经被获取,其余 $G$ 在获取写锁时会进入自旋或者休眠
  • 调用 sync/atomic.AddInt32 函数阻塞后续的读操作
  • 如果依然有其余 $G$ 持有互斥锁的读锁,该 $G$ 会调用 runtime.sync_runtime_SemacquireMutex 进入休眠状态期待所有读锁的所有者执行完结后开释 writeSem 信号量将以后协程唤醒

写锁的开释会调用sync.RWMutex.Unlock

func (rw *RWMutex) Unlock() {
    ...
    // 将 readerCount 的值减少 rwmutexMaxReaders,使 readerCount 变为非正数,宣告有读操作行将完结
    r := atomic.AddInt32(&rw.readerCount, rwmutexMaxReaders)
    if r >= rwmutexMaxReaders {
        ...
        throw("sync: Unlock of unlocked RWMutex")
    }
    for i := 0; i < int(r); i++ {runtime_Semrelease(&rw.readerSem, false, 0)
    }
    rw.w.Unlock()
    ...
}

与加锁的过程正好相同,写锁的开释分为以下几步:

  • 调用 sync/atomic.AddInt32 函数将 readerCount 变回负数,开释读锁
  • 通过 for 循环开释所有因为获取读锁而陷入期待的 $G$
  • 调用 sync.Mutex.Unlock 开释写锁

获取写锁时会先阻塞写锁的获取,后阻塞读锁的获取,这种策略可能保障读操作不会被间断的写操作“饿死”。

3 读锁

读锁的加锁办法 sync.RMWutex.RLock 很简略,该办法会通过 sync/actomic.AddInt32readerCount加一:

func (rw *RWMutex) RLock() {
    ...
    if atomic.AddInt32(&rw.readerCount, 1) < 0 {
        // 有一个写操作未实现,期待它执行结束
        runtime_SemacquireMutex(&rw.readerSem, false, 0)
    }
    ...
}
  • 如果该办法返回正数,意味着有其余 $G$ 取得了写锁,以后 $G$ 就会调用 runtime.sync_runtime_SemacquireMutex 陷入休眠期待锁的开释
  • 如果该办法的后果为非正数,意味着没有 $G$ 取得写锁,以后办法会胜利返回

当 $G$ 想要开释读锁时,会调用如下所示的 sync.RMWutex.RUnlock 办法:

func (rw *RWMutex) RUnlock() {
    ...
    if r := atomic.AddInt32(&rw.readerCount, -1); r < 0 {
        // Outlined slow-path to allow the fast-path to be inlined
        rw.rUnlockSlow(r)
    }
    ...
}

该办法会先缩小正在读资源的 readerCount 整数,依据 sync/atomic.AddInt32 的返回值不同会别离进行解决:

  • 如果返回值大于等于 0,读锁间接解锁胜利
  • 如果返回值小于 0,示意有一个未实现的写操作,这时会调用 sync.RWMutex.rUnlockSlow 办法
func (rw *RWMutex) rUnlockSlow(r int32) {
  if r+1 == 0 || r+1 == -rwmutexMaxReaders {
    ...
    throw("sync: RUnlock of unlocked RWMutex")
  }
  // 有一个写操作未实现
  if atomic.AddInt32(&rw.readerWait, -1) == 0 {runtime_Semrelease(&rw.writerSem, false, 1)
  }
}

sync.RWMutex.rUnlockSlow会缩小获取锁的写操作期待的读操作数 readerWait 并在所有读操作都被开释之后触发写操作的信号量writerSem,该信号量被触发时,调度器就会唤醒尝试获取写锁的 $G$。

4 小结

尽管读写互斥锁 sync.RMWutex 提供的性能比较复杂,但它是建设在 sync.Mutex 的基出上,所以代码实现很简略。

读锁和写锁的关系:

  1. 调用 sync.RMWutex.Lock 尝试获取锁时

    • 每次 sync.RMWutex.RUlock 都会将 readerCount 减一,当它归零时该 $G$ 会取得写锁
    • readerCount 缩小 rwmutexMaxReaders 以阻塞后续的读操作
  2. 调用 sync.RWMutex.Unlock 开释写锁时,会先告诉所有的读操作,而后才会开释持有的互斥锁

读写互斥锁在互斥锁之外提供了额定的更细粒度的管制,可能在读操作远远多于写操作时晋升性能。

三、WaitGourp

sync.WaitGroup能够期待一组 $G$ 的返回,一个比拟常见的应用场景是批量收回 RPC 或者 HTTP 申请:

requests := []*Request{...}
wg := sync.WaitGroup{}
wg.add(len(requests))

for _, request := range requests {go func(r *request) {defer wg.Done()
    ...
  }(request)
}

wg.Wait()

能够通过 sync.WaitGroup 将本来程序执行的代码在多个 $G$ 中并发执行,放慢程序处理速度。

1 构造体

type WaitGroup struct {
    noCopy noCopy  // 保障 wg 不会被开发者通过再赋值的形式拷贝
    state1 [3]uint32  // 存储状态和信号量
}

sync.noCopy是一个公有构造体,在编绎时会查看被拷贝的变量中是否蕴含 sync.noCopy 或者实现了 LockUnlock办法。如果蕴含该构造体或者实现了对应的办法就会报出以下谬误:

func main() {wg := sync.WaitGroup{}
    wg2 := wg
    fmt.Println(wg, wg2)
}

$ go vet main.go 
# command-line-arguments
./main.go:10:9: assignment copies lock value to wg2: sync.WaitGroup contains sync.noCopy
./main.go:11:14: call of fmt.Println copies lock value: sync.WaitGroup contains sync.noCopy
./main.go:11:18: call of fmt.Println copies lock value: sync.WaitGroup contains sync.noCopy

这段代码会因为变量赋值或调用函数时产生值拷贝导致分析器报错。

sync.state1的代码正文:

    // 64-bit value: high 32 bits are counter, low 32 bits are waiter count.
    // 64-bit atomic operations require 64-bit alignment, but 32-bit
    // compilers do not ensure it. So we allocate 12 bytes and then use
    // the aligned 8 bytes in them as state, and the other 4 as storage
    // for the sema.

sync.WaitGroup提供的公有办法 sync.WaitGroup.state 可能帮咱们从 state1 字段中取出它的状态和信号量。

2 接口

sync.WaitGroup对外裸露了三个办法:AddWaitDone

其中 Done 办法只是向 Add 中传入了 -1,所以重点剖析另外两个办法 AddWait

2.1 Add

func (wg *WaitGroup) Add(delta int) {statep, semap := wg.state()
    ...
    state := atomic.AddUint64(statep, uint64(delta)<<32)
    v := int32(state >> 32)
    w := uint32(state)
    ...
    if v < 0 {panic("sync: negative WaitGroup counter")
    }
    if w != 0 && delta > 0 && v == int32(delta) {panic("sync: WaitGroup misuse: Add called concurrently with Wait")
    }
    if v > 0 || w == 0 {return}
    if *statep != state {panic("sync: WaitGroup misuse: Add called concurrently with Wait")
    }
    *statep = 0
    for ; w != 0; w-- {runtime_Semrelease(semap, false, 0)
    }
}

Add办法向可能是正数的 WaitGroupcounter上减少增量。

如果 counter 归零,所有 Wait 的被阻塞的 $G$ 都被开释。

如果 counter 是正数,会引发 panic。

2.2 Wait

func (wg *WaitGroup) Wait() {statep, semap := wg.state()
    ...
    for {state := atomic.LoadUint64(statep)
        v := int32(state >> 32)
        w := uint32(state)
        if v == 0 {
            ...
            return
        }
        if atomic.CompareAndSwapUint64(statep, state, state+1) {
            ...
            runtime_Semacquire(semap)
            if *statep != 0 {panic("sync: WaitGroup is reused before previous Wait has returned")
            }
            ...
            return
        }
    }
}

Wait的作用就是在 WaitGroupcounter归零前始终阻塞。

3 小结

  • sync.WaitGroup必须在 sync.WaitGroup.Wait 办法返回之后能力被从新应用
  • sync.WaitGroup.Done只是向 sync.WaitGroup.Add 办法传入 -1 以唤醒期待的 $G$。所以也能够通过向 Add 内传递一个正数来代替Done
  • 能够同时有多个 $G$ 期待以后 sync.WaitGroup 计数器归零,这些 $G$ 会被同时唤醒

四、Once

sync.Once能够保障程序运行期间某段代码只执行一次。

简略示例:

func main() {o := sync.Once{}
    for i := 0; i < 10; i++ {o.Do(func() {fmt.Println("once")
        })
    }
}

$ go run main.go 
once

1 构造体

type Once struct {
    done uint32  // 代码是否执行过的标识
    m    Mutex  // 互斥锁
}

2 接口

sync.Once.Dosync.Once 构造体对外裸露的惟一的办法,该办法会接管一个入参为空的函数:

  • 如果传入的函数曾经执行过,会间接返回
  • 如果传入的函数没有执行过,会调用 sync.Once.doSlow 执行传入的函数
func (o *Once) Do(f func()) {if atomic.LoadUint32(&o.done) == 0 {o.doSlow(f)
    }
}

func (o *Once) doSlow(f func()) {o.m.Lock()
    defer o.m.Unlock()
    if o.done == 0 {defer atomic.StoreUint32(&o.done, 1)
        f()}
}

执行过程:

  1. 为以后 $G$ 上锁
  2. 执行传入的无入参函数
  3. 运行提早函数调用,将成员变量 done 更新成 1

sync.Once会通过成员变量 done 确保函数不会执行第二次。

3 小结

作为用于保障函数执行次数的 sync.Once 构造体,应用了互斥锁 sync/atomic 包提供的办法实现了某个函数在程序运行期间只能执行一次的语义。

在应用该构造体时,也须要留神以下问题:

  • sync.Once.Do办法中传入的函数只会被执行一次,哪怕函数中产生了panic
  • 两次调用 sync.Once.Do 办法传入不同的函数只会执行第一次传入的函数

五、Cond

sync.Cond是条件变量,能够让一组 $G$ 都在满足特定条件时被唤醒。每一个 sync.Cond 构造体在初始化时都须要传入一个互斥锁。

简略示例:

var status uint32

func listen(c *sync.Cond) {c.L.Lock()
    for atomic.LoadUint32(&status) != 1 {c.Wait()
    }
    fmt.Println("listenning")
    c.L.Unlock()}

func broadcast(c *sync.Cond) {c.L.Lock()
    atomic.StoreUint32(&status, 1)
    c.Broadcast()
    c.L.Unlock()}

func main() {c := sync.NewCond(&sync.Mutex{})
    for i := 0; i < 10; i++ {go listen(c)
    }
    time.Sleep(time.Second)
    go broadcast(c)

    ch := make(chan os.Signal, 1)
    signal.Notify(ch, os.Interrupt)
    <-ch
}

上述代码同时运行了 11 个 $G$,这 11 个 $G$ 别离做了不同的事:

  • 10 个 $G$ 通过 sync.Cond.Wait 期待特定条件的满足
  • 1 个 $G$ 会调用 sync.Cond.Broadcast 唤醒所有陷入期待的 $G$

调用 sync.Cond.Broadcast 办法后,上述代码会打印出 10 次 “listenning” 并完结调用。

1 构造体

type Cond struct {
    noCopy noCopy  // 保障构造体不会在编绎时拷贝
    L Locker  // 爱护外部的 `notify` 字段
    notify  notifyList  // 一个 Goroutine 链表,实现同步机制的外围构造
    checker copyChecker  // 禁止运行期间产生拷贝
}

2 接口

2.1 Wait

sync.Cond对外裸露的 sync.Cond.Wait 办法会将以后 $G$ 陷入休眠状态,它的执行过程分成以下两个步骤:

  • 调用 runtime.notifyListAdd 将期待计数器加一并解锁
  • 调用 runtime.notifyListWait 期待其余 $G$ 的唤醒并加锁
func (c *Cond) Wait() {c.checker.check()
    t := runtime_notifyListAdd(&c.notify)
    c.L.Unlock()
    runtime_notifyListWait(&c.notify, t)
    c.L.Lock()}

2.2 Signal 和 Broadcast

sync.Cond.Signalsync.Cond.Broadcast 就是用来唤醒陷入休眠的 $G$ 的办法,它们的实现有一些轻微的差异:

  • Signal办法会唤醒队列最后面的 $G$
  • Broadcast办法会唤醒队列中全副的 $G$
func (c *Cond) Signal() {c.checker.check()
    runtime_notifyListNotifyOne(&c.notify)
}

func (c *Cond) Broadcast() {c.checker.check()
    runtime_notifyListNotifyAll(&c.notify)
}

$G$ 的唤醒程序也是依照退出队列的先后顺序,先退出的会先被唤醒,而后退出的可能须要期待调度器的调度。

个别状况下,咱们都会先调用 sync.Cond.Wait 陷入休眠期待满足冀望条件,当满足唤醒条件时,就能够抉择应用 sync.Cond.Signal 或者 sync.Cond.Broadcast 唤醒一个或者全副的 $G$。

3 小结

sync.Cond不是一个罕用的同步机制,然而在条件长时间无奈满足时,与应用 for {} 进行繁忙期待相比,sync.Cond可能让出处理器的使用权,提供 CPU 的利用率。应用时须要留神以下问题:

  • Wait在调用之前肯定要上锁,否则会触发panic,程序解体
  • Signal唤醒的 $G$ 都是队列最后面、期待最久的 $G$
  • Broadcast会依照肯定程序播送告诉期待的全副 $G$
退出移动版