一起读Go源码(第15期) sync:Once与Cond
概述
朋友们好,这篇笔记继续读Go sync包剩余部分Once、Cond下面来看看。
Once
Once是sync包中的并发安全工具,核心用途是保证某个函数或代码块在程序周期内只执行一次,具体来说Once具有并发安全,只执行一次、执行失败也不再执行和没有返回值的特征,如在单例模式下全局下唯一配置数据库、初始化配置这些场景下有用;Once提供了Do方法,该方法接收一个无参无返回的函数f,在第一次调用Do(f)才会执行f,后续调用都会略过,下面来看看例子,在waitGroup下并发调用十次once.Do(f),最后函数只执行了一次 。
var once sync.Oncefunc main() { wg := sync.WaitGroup{} wg.Add(10) for i := 0; i < 10; i++ { gofunc(i int) { defer wg.Done() once.Do(initConfig) }(i) } wg.Wait()}func initConfig() { log.Println("初始化配置")}// 2026/04/13 20:51:18 初始化配置
Once结构体解析
Once是一个只会执行一次动作的对象,其实例在第一次使用之后被禁止复制,在Go内存墨西哥中,f函数的返回同步优先于任何Once.Do(f)的返回(这句话的意思是f函数要先执行完,Do()函数才能返回,不会出现正在执行的情况,当Do()返回时说明f已经执行完了)。Once结构体的两个字段是done和m,其中
-
• done用来标记是否执行过,是原子无符号整数类型`atomic.Uint32“ -
• m 是互斥锁用于保证并发安全。
源码如下:
// Once is an object that will perform exactly one action.//// A Once must not be copied after first use.//// In the terminology of [the Go memory model],// the return from f “synchronizes before”// the return from any call of once.Do(f).//// [the Go memory model]: https://go.dev/ref/memtype Once struct { // done indicates whether the action has been performed. // It is first in the struct because it is used in the hot path. // The hot path is inlined at every call site. // Placing done first allows more compact instructions on some architectures (amd64/386), // and fewer instructions (to calculate offset) on other architectures. done atomic.Uint32 m Mutex}
Once Do方法解析
Do方法实现分为两种路径,快路径和慢路径;如果o.load.Load() == 1说明已经执行过了,这条是快路径,若o.load.Load() == 0匹配到,说明还没执行过,走慢路径。
// Do calls the function f if and only if Do is being called for the// first time for this instance of [Once]. In other words, given//// var once Once//// if once.Do(f) is called multiple times, only the first call will invoke f,// even if f has a different value in each invocation. A new instance of// Once is required for each function to execute.//// Do is intended for initialization that must be run exactly once. Since f// is niladic, it may be necessary to use a function literal to capture the// arguments to a function to be invoked by Do://// config.once.Do(func() { config.init(filename) })//// Because no call to Do returns until the one call to f returns, if f causes// Do to be called, it will deadlock.//// If f panics, Do considers it to have returned; future calls of Do return// without calling f.func (o *Once) Do(ffunc()) { // Note: Here is an incorrect implementation of Do: // // if o.done.CompareAndSwap(0, 1) { // f() // } // // Do guarantees that when it returns, f has finished. // This implementation would not implement that guarantee: // given two simultaneous calls, the winner of the cas would // call f, and the second would return immediately, without // waiting for the first's call to f to complete. // This is why the slow path falls back to a mutex, and why // the o.done.Store must be delayed until after f returns. if o.done.Load() == 0 { // Outlined slow-path to allow inlining of the fast-path. o.doSlow(f) }}func (o *Once) doSlow(ffunc()) { o.m.Lock() defer o.m.Unlock() if o.done.Load() == 0 { defer o.done.Store(1) f() }}
若进入doSlow函数后首先是加互斥锁,然后再检查一遍是否真的没执行过 o.done.Load() == 0,执行一遍函数f后,将f设置为已执行1defer o.done.Store(1)。
这时候有彦祖要问了,init也是在程序启动时执行,它们之间有什么区别?首先它们的相同点在于都只执行一次,且都是并发安全;但init是在包加载时自动执行,意思是控制不了什么时候执行。Once可以控制啥时候执行,或者不调用就不执行。
Cond
Cond是sync提供的条件变量,核心用途是让goroutine等待某个条件成立,条件满足后再唤醒等待的goroutine;当条件不满足时goroutine调用Wait()进入休眠(此时不占用CPU),等条件满足时调用Signal()或Broadcast()唤醒休眠的goroutine;Cond提供了3个方法来阻塞和唤醒goroutine:
-
1. Wait()阻塞等待 -
2. Signal唤醒一个等待的goroutine -
3. Broadcast唤醒所有等待的goroutine
下面模拟一个场景,同时有3个goroutine等待读取一个数据,该数据初始状态是被禁止读取,用Wait将这3个goroutine先休眠,sleep 2s后data设置成能被读取了,再使用Broadcast唤醒全部goroutine;当然了如果使用Signal只会唤醒一个goroutine,打印1条goroutine读取数据的消息;可以看到在data设置为true以前,3个goroutine都设置休眠,使用Broadcast唤醒全部goroutine后都读取数据了。
var mu sync.Mutexcond := sync.NewCond(&mu)data := falsefor i := 0; i < 3; i++ { gofunc(id int) { mu.Lock() defer mu.Unlock() for !data { log.Println("goroutine", id, "已休眠") cond.Wait() } log.Println("goroutine ", id, "已读取数据") }(i)}time.Sleep(2 * time.Second)mu.Lock()data = truelog.Println("data此时可被读取")mu.Unlock()cond.Broadcast() // 唤醒所有等待的goroutinetime.Sleep(2 * time.Second)
2026/04/13 21:09:41 goroutine 0 已休眠2026/04/13 21:09:41 goroutine 2 已休眠2026/04/13 21:09:41 goroutine 1 已休眠2026/04/13 21:09:43 data此时可被读取2026/04/13 21:09:43 goroutine 1 已读取数据2026/04/13 21:09:43 goroutine 0 已读取数据2026/04/13 21:09:43 goroutine 2 已读取数据
Cond 结构体解析
查看Cond源码,它实现了一个条件变量,它是goroutine之间用于等待时间发生或通知事件发生的汇合点;每个Cond都关联了一个互斥锁(通常是互斥锁或读写锁),在修改条件状态以及调用Wait方法时必须持有该锁。Cond在首次使用后不允许被复制。对于绝大部分简单场景,使用channel会比Cond更合适;在Cond结构体的几个字段:
-
1. noCopy:禁止复制标记 -
2. L:绑定的互斥锁 -
3. notify:等待队列,存放所有休眠的goroutine -
4. checker:运行时复制检查器
创建一个Cond实例时,需要传入一个互斥锁。
type Cond struct { noCopy noCopy // L is held while observing or changing the condition L Locker notify notifyList checker copyChecker}// NewCond returns a new Cond with Locker l.func NewCond(l Locker) *Cond { return &Cond{L: l}}
Cond Wait方法解析
Wait方法会原子解锁c.l,并挂起当前调用方法的goroutine。当该goroutine之后恢复执行时,Wait会在返回前重新锁住c.L;与其他系统不同,Wait除非被Cond.Broadcast或Cond.Signal唤醒,否则不会返回,因为 Wait 在等待期间不会持有 c.L 锁,所以调用者通常不能在 Wait 返回时就直接认定条件已经成立——正确的做法是调用者应在循环中使用Wait。
// Wait atomically unlocks c.L and suspends execution// of the calling goroutine. After later resuming execution,// Wait locks c.L before returning. Unlike in other systems,// Wait cannot return unless awoken by [Cond.Broadcast] or [Cond.Signal].//// Because c.L is not locked while Wait is waiting, the caller// typically cannot assume that the condition is true when// Wait returns. Instead, the caller should Wait in a loop://// c.L.Lock()// for !condition() {// c.Wait()// }// ... make use of condition ...// c.L.Unlock()func (c *Cond) Wait() { c.checker.check() t := runtime_notifyListAdd(&c.notify) c.L.Unlock() runtime_notifyListWait(&c.notify, t) c.L.Lock()}func (c *copyChecker) check() { // Check if c has been copied in three steps: // 1. The first comparison is the fast-path. If c has been initialized and not copied, this will return immediately. Otherwise, c is either not initialized, or has been copied. // 2. Ensure c is initialized. If the CAS succeeds, we're done. If it fails, c was either initialized concurrently and we simply lost the race, or c has been copied. // 3. Do step 1 again. Now that c is definitely initialized, if this fails, c was copied. if uintptr(*c) != uintptr(unsafe.Pointer(c)) && !atomic.CompareAndSwapUintptr((*uintptr)(c), 0, uintptr(unsafe.Pointer(c))) && uintptr(*c) != uintptr(unsafe.Pointer(c)) { panic("sync.Cond is copied") }}
Wait方法来就做几件事:
-
1. 检查 Cond是否被复制,若被复制会panic掉(贴了check函数代码); -
2. 将当前goroutine加入等待队列notify; -
3. c.L.Unlock()解锁 -
4. 将当前goroutine挂起,使其进入休眠,先睡了,下面的代码睡醒再继续执行; -
5. c.L.Lock(),当goroutine被唤醒后(Signal或Broadcast唤醒),上锁
Cond Signal和Broadcast 解析
如果有等待者,Signal会唤醒一个正在等待的goroutine;调用该方法时,调用者允许但不要求持有c.L锁;Signal方法首先会检查Cond是否被复制,否则会panic掉(跟上面一样),runtime_notifyListNotifyOne会唤醒等待队列里的1个goroutine(先进先出原则);
// Signal wakes one goroutine waiting on c, if there is any.//// It is allowed but not required for the caller to hold c.L// during the call.//// Signal() does not affect goroutine scheduling priority; if other goroutines// are attempting to lock c.L, they may be awoken before a "waiting" goroutine.func (c *Cond) Signal() { c.checker.check() runtime_notifyListNotifyOne(&c.notify)}
Broadcast与Signal类似,先检查Cond是否被复制,然后runtime_notifyListNotifyAll(&c.notify)唤醒队列里休眠的全部goroutine。
// Broadcast wakes all goroutines waiting on c.//// It is allowed but not required for the caller to hold c.L// during the call.func (c *Cond) Broadcast() { c.checker.check() runtime_notifyListNotifyAll(&c.notify)}
与channel对比
回到上面Cond源码中的一句话:For many simple use cases, users will be better off using channels than a Cond )对于绝大部分简单场景,使用channel会比Cond更合适。在Go中Cond和Channel都用于并发同步:
-
• Channel通道更偏向的是通信的同步机制,在协程间安全传递数据,有发送有接收;同时也因为线程安全必须额外加互斥锁来保证并发安全。 ch := make(chan string)gofunc() { ch <- "外星人:收到" // 发送数据到通道}()//接收数据msg := <-chfmt.Println(msg) -
• Cond更偏向条件等待的同步机制,通过互斥锁保证并发安全;且不能传输数据,只有唤醒/休眠的信号。
结合场景考虑,Cond的最舒服的场景是当数据从空变到有数据时,此时唤醒所有等待的goroutine来消费数据;Channel通道最舒服的场景是只需要通知一下,具体协程就开始工作的场景。
那写得差不多了,先这样了各位bro。
写在最后
本人是新手小白,如果这篇笔记中有任何错误或不准确之处,真诚地希望各位读者能够给予批评和指正,如有更好的实现方法请给我留言,谢谢!欢迎大家在评论区留言!觉得写得还不错的话欢迎大家关注一波!
夜雨聆风