一.sync.Once
Once
(单次执行)
用途:确保某个操作只执行一次(如初始化配置)
核心方法:Do(f func())
:保证 f
只执行一次
package main
import (
"fmt"
"sync"
)
var (
config map[string]string
once sync.Once
wg sync.WaitGroup
)
func loadConfig() {
once.Do(func() {
fmt.Println("Loading config...")
config = map[string]string{"key": "value"}
})
}
func main() {
for i := 0; i < 5; i++ {
wg.Add(1)
go func() {
defer wg.Done() // 确保 goroutine 结束时减少计数器
loadConfig()
}()
}
wg.Wait() // 等待所有 goroutine 完成
}
可以确保这个协程只进行一次
二.sync.Cond
sync.Cond 是 golang 标准库提供的并发协调器,用于支援开放人员在指定条件下阻塞和唤醒协程的操作.
Wait()
:释放锁并阻塞,直到被唤醒。Signal()
:唤醒一个等待的 goroutine。Broadcast()
:唤醒所有等待的 goroutine。
2.1 数据结构与构造器方法
type Cond struct {
// 不可以对其进行值拷贝
noCopy noCopy
// 一个自旋锁
L Locker
// 一个队列,存放阻塞的goroutine
notify notifyList
checker copyChecker
}
// NewCond returns a new Cond with Locker l.
func NewCond(l Locker) *Cond {
return &Cond{L:l}
}
(1)成员变量 noCopy + checker 是一套组合拳,保证 Cond 在第一次使用后不允许被复制;
(2)核心变量 L,一把锁,用于实现阻塞操作;
(3)核心变量 notify,阻塞链表,分别存储了调用 Cond.Wait() 方法的次数、goroutine 被唤醒的次数、一把系统运行时的互斥锁以及链表的头尾节点.
type notifyList struct {
wait uint32
notify uint32
lock uintptr // key field of the mutex
head unsafe.Pointer
tail unsafe.Pointer
}
2.2 Cond.Wait
作用:把当前这个持有锁的goroutine,释放锁,陷入一个被动阻塞的状态,加入阻塞队列里面。
什么时候被唤醒呢?
当有其他的goroutine也持有这个Cond的引用,使用Signal函数的时候,会首先唤醒队首的goroutine
通过这个机制就可以实现异步goroutine的协调,比如需要某一个goroutine实现了某一个动作,另外一个goroutine才可以继续执行的一个场景。
使用的前置条件
看下面的代码我们会发现,它有一个解锁的操作,所以在调用他之前,必须是加锁的状态,只有这样才可以执行,然后陷入被动阻塞的状态,阻塞唤醒之后,才会重新加锁。
func (c *Cond) Wait() {
c.checker.check()
t := runtime_notifyListAdd(&c.notify)
c.L.Unlock()
runtime_notifyListWait(&c.notify, t)
c.L.Lock()
}
(1)检查 Cond 是否在使用过后被拷贝,是则 panic;
(2)该 Cond 阻塞链表 wait 统计数加 1;
(3)当前协程释放锁,因为接下来即将被操作系统 park;
(4)将当前协程包装成节点,添加到 Cond 的阻塞队列当中,并调用 park 操作将当前协程挂起;
(5)协程被唤醒后,重新尝试获取锁.
2.3 Cond.Signal
作用:就是唤醒队首的goroutine唤醒
func (c *Cond) Signal() {
c.checker.check()
runtime_notifyListNotifyOne(&c.notify)
}
(1)检查 Cond 是否在首次使用后被拷贝,是则 panic;
(2)该 Cond 阻塞链表 notify 统计数加 1;
(3)从头开始遍历阻塞链表,唤醒一个等待时间最长的 goroutine.
2.4 Cond.BroadCast
作用:就是将阻塞队列里面的所有goroutine都进行唤醒。
func (c *Cond) Broadcast() {
c.checker.check()
runtime_notifyListNotifyAll(&c.notify)
}
(1)检查 Cond 是否在首次使用后被拷贝,是则 panic;
(2)取 wait 值赋值给 notify;
(3)唤醒阻塞链表所有节点.
2.5 使用案例
package main
import (
"fmt"
"sync"
"time"
)
func main() {
var mu sync.Mutex
cond := sync.NewCond(&mu)
// 共享状态
isReady := false
// 等待条件的goroutine
go func() {
fmt.Println("等待者: 等待条件满足...")
cond.L.Lock()
defer cond.L.Unlock()
// 使用循环防止虚假唤醒
for !isReady {
cond.Wait() // 释放锁并阻塞,唤醒时会重新获得锁
fmt.Println("等待者: 被唤醒,检查条件")
}
fmt.Println("等待者: 条件已满足!")
}()
// 改变条件的goroutine
go func() {
time.Sleep(2 * time.Second) // 模拟耗时操作
fmt.Println("触发者: 准备改变条件...")
cond.L.Lock()
isReady = true
cond.L.Unlock()
fmt.Println("触发者: 发送通知")
cond.Signal() // 唤醒一个等待的goroutine
}()
time.Sleep(3 * time.Second) // 等待所有goroutine完成
}