sync包提供了如互斥锁之类的基本同步原语,除了Once和WaitGroup类型外,大多数是供低级库例程使用的。通过通道和通信可以更好地实现更高级别的同步。
包中定义的类型的值,都不应该被复制
Cond实现类一个条件变量,用于gorountines等待或者通知事件发生的集合点。
type Cond struct {
noCopy noCopy // 标识不能被复制
// L is held while observing or changing the condition
L Locker // 每个Cond都有一个关联的Locker L(通常是Mutex或RWMutex),在改变条件和调用Wait方法时必须保持它。
notify notifyList // 通知列表
checker copyChecker //复制检查器
}
用于构造一个Cond结构体对象,无需多言~
// NewCond returns a new Cond with Locker l.
func NewCond(l Locker) *Cond {
return &Cond{L: l}
}
Wait方法会以原子地方式解锁c.L
并暂停调用者goroutine的执行(进入等待状态);当后续恢复执行时,Wait方法会在返回前锁住c.L
。
func (c *Cond) Wait() {
c.checker.check() // 检查Cond是否被复制
t := runtime_notifyListAdd(&c.notify) // 添加通知对象
c.L.Unlock() // 解锁
runtime_notifyListWait(&c.notify, t)
c.L.Lock() // 加锁
}
需要特别注意的是:由于等待时,c.L是没有上锁的,通常情况下,调用者不能假定当Wait方法返回时,等待的条件是满足的,因此调用者应该在循环中等待。
c.L.Lock()
for !condition() {
c.Wait()
}
... make use of condition ...
c.L.Unlock()
如果在调用 sync.Cond 类型的 Wait 方法时不在循环中检查条件,可能会导致一些问题,主要是因为条件变量的特性和工作原理:在调用 Wait 方法时,条件变量会释放关联的互斥锁,并阻塞当前协程,直到另一个协程调用 Signal 或 Broadcast 方法来通知条件变量。在通知后,等待的协程会被唤醒,并尝试重新获取互斥锁。如果不在循环中检查条件,等待的协程可能会在条件不满足的情况下被唤醒,然后继续执行后续逻辑,这可能导致程序行为不符合预期。可能出现的问题:
竞态条件:如果在等待协程被唤醒后不再检查条件就继续执行后续逻辑,可能会导致竞态条件的发生,从而导致程序出现不确定的行为。
条件不满足:如果在唤醒后直接执行后续逻辑而不检查条件,可能会导致程序在条件不满足的情况下继续执行,从而产生错误的结果。
协程阻塞:如果不在循环中调用 Wait 方法,等待的协程可能会在条件不满足的情况下被唤醒,但由于条件仍未满足,协程会继续等待,导致协程长时间阻塞。
因此,为了避免这些问题,通常建议在调用 sync.Cond 类型的 Wait 方法时使用循环来检查条件。这样可以确保条件满足时才继续执行后续逻辑,避免竞态条件和不确定行为的发生。
Signal方法会唤醒一个等待的goroutine(如果有的话)。
func (c *Cond) Signal() {
c.checker.check()
runtime_notifyListNotifyOne(&c.notify)
}
在 Go 语言中,sync.Cond 类型的 Signal 方法会唤醒一个等待在条件变量上的 goroutine,但它并不会影响 goroutine 的调度优先级。这意味着使用 Signal 方法唤醒的 goroutine 和其他正常运行的 goroutine 之间不会有优先级上的差别。
此外,当调用 Signal 方法唤醒一个 goroutine 时,并不保证该 goroutine 会立即获取到条件变量关联的互斥锁。其他 goroutine 可能在此时尝试锁定互斥锁,并且它们可能会在被唤醒的 goroutine 之前获取到锁。因此,被唤醒的 goroutine 可能需要等待其他 goroutine 释放锁之后才能继续执行。
这种情况可能会导致一些竞态条件的发生,因为被唤醒的 goroutine 并不会立即执行,而是需要等待互斥锁。其他 goroutine 可能会在此期间修改共享状态,从而影响被唤醒的 goroutine 的行为。
因此,在使用 sync.Cond 类型时,需要特别注意在调用 Signal 方法后被唤醒的 goroutine 和其他竞争锁的 goroutine 之间可能存在的竞态条件。正确的处理方式应该是在唤醒的 goroutine 中重新检查条件并获取互斥锁,以确保在正确的条件下执行后续逻辑。
Broadcast会唤醒所有在 c 上等待的 goroutine。
func (c *Cond) Broadcast() {
c.checker.check()
runtime_notifyListNotifyAll(&c.notify)
}
可以用Cond来实现生产者、消费者模式。(实际不会这么写,此处只是为了演示Cond的用法)
var (
mux = sync.Mutex{} // 互斥锁
cond = sync.NewCond(&mux) // 条件变量
queue []int // 消息队列
)
// producer 生产者
func producer(count int) {
for i := 0; i < count; i++ {
mux.Lock() // 获取锁
queue = append(queue, i+1) // 生产消息
fmt.Printf("produce %d.\n", i+1)
cond.Signal() // 通知消费者
mux.Unlock() // 解锁
}
}
// consumer 消费者
func consumer() {
for {
mux.Lock() // 获取锁
for len(queue) == 0 {
cond.Wait() // 如果没有消息就等待
}
fmt.Printf("consume %d.\n", queue[0])
queue = queue[1:]
mux.Unlock() // 解锁
}
}
func main() {
go producer(10)
go consumer()
time.Sleep(time.Second)
}
可以使用 sync.Cond 让多个协程等待某个任务的完成。任务完成后通过 Broadcast 方法通知所有等待的协程。
var (
mu sync.Mutex
cond = sync.NewCond(&mu)
done bool
)
func worker(id int) {
mu.Lock()
for !done {
cond.Wait()
}
fmt.Println("Worker", id, "completed")
mu.Unlock()
}
func main() {
for i := 0; i < 5; i++ {
go worker(i)
}
// 模拟任务完成
mu.Lock()
done = true
cond.Broadcast()
mu.Unlock()
// 主程序等待一段时间,让协程完成输出
// 这里只是为了示例,实际情况可能需要更复杂的逻辑
select {}
}
Locker是所有“锁”的接口,如Mutex、RWMutex等,这就不需要多说了。
A Locker represents an object that can be locked and unlocked.
type Locker interface {
Lock()
Unlock()
}
Mutex是一个互斥锁。
特别注意:Mutex是不可重入的互斥锁!!!
type Mutex struct {
state int32
sema uint32
}
Mutex有两种操作模式:
正常模式的性能要优于饥饿模式,因为在正常模式下,一个 goroutine 可以连续多次获取锁,即使存在被阻塞的等待者。饥饿模式的作用是防止尾部延迟的极端情况。
Go 标准库中的 sync.Mutex 实现了这样的模式切换机制,以兼顾性能和避免饥饿情况。这种设计可以确保在大多数情况下,正常模式下的性能表现良好,同时避免极端情况下的饥饿问题。
Lock方法用于获取互斥锁,如果互斥锁已经被占用,调用者将被阻塞,直到互斥锁可用。
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()
}
TryLock方法会获取互斥锁,并返回是否获取互斥锁成功。
请注意,虽然 TryLock 的正确使用确实存在,但很少见,并且 TryLock 的使用通常表明互斥体的特定使用中存在更深层次的问题。
func (m *Mutex) TryLock() bool {
old := m.state
if old&(mutexLocked|mutexStarving) != 0 {
return false
}
// There may be a goroutine waiting for the mutex, but we are
// running now and can try to grab the mutex before that
// goroutine wakes up.
if !atomic.CompareAndSwapInt32(&m.state, old, old|mutexLocked) {
return false
}
if race.Enabled {
race.Acquire(unsafe.Pointer(m))
}
return true
}
Unlock方法将释放互斥锁(解锁)。
func (m *Mutex) Unlock() {
if race.Enabled {
_ = m.state
race.Release(unsafe.Pointer(m))
}
// Fast path: drop lock bit.
new := atomic.AddInt32(&m.state, -mutexLocked)
if new != 0 {
// Outlined slow path to allow inlining the fast path.
// To hide unlockSlow during tracing we skip one extra frame when tracing GoUnblock.
m.unlockSlow(new)
}
}
在Cond条件变量章节的示例中,就有mutex的使用,请参考Cond条件变量章节。
RWMutex是一个读写互斥锁,该锁可以被任意数量的读锁或者单个写锁持有。
需要注意的是,sync.RWMutex 不支持递归的读取锁定,即同一个 goroutine 在持有读取锁的情况下再次尝试获取读取锁会导致死锁。这是为了避免潜在的死锁情况,强制要求编写代码时注意锁的持有情况。【我在实际测试过程中,发现同一个线程是可以重复获取读锁的。但是如果在递归中获取读锁,而又有其他协程获取写锁,可能会导致死锁。】
源码注释的原文为:
// If any goroutine calls Lock while the lock is already held by
// one or more readers, concurrent calls to RLock will block until
// the writer has acquired (and released) the lock, to ensure that
// the lock eventually becomes available to the writer.
// Note that this prohibits recursive read-locking.
type RWMutex struct {
w Mutex // held if there are pending writers
writerSem uint32 // semaphore for writers to wait for completing readers
readerSem uint32 // semaphore for readers to wait for completing writers
readerCount atomic.Int32 // number of pending readers
readerWait atomic.Int32 // number of departing readers
}
RLock 锁定 rw 以进行读取。它不应该用于递归读锁定;阻塞的 Lock 调用会阻止新的读取者获取锁。
func (rw *RWMutex) RLock() {
...
}
TryRLock尝试获取读锁并返回是否获取成功。
// Note that while correct uses of TryRLock do exist, they are rare,
// and use of TryRLock is often a sign of a deeper problem
// in a particular use of mutexes.
func (rw *RWMutex) TryRLock() bool {
...
}
RUnlock释放单个读锁,对剩余的读者不影响。
func (rw *RWMutex) RUnlock() {
...
}
RLocker 返回一个 Locker 接口,通过调用 rw.RLock 和 rw.RUnlock 实现 Lock 和 Unlock 方法。
func (rw *RWMutex) RLocker() Locker {
return (*rlocker)(rw)
}
type rlocker RWMutex
func (r *rlocker) Lock() { (*RWMutex)(r).RLock() }
func (r *rlocker) Unlock() { (*RWMutex)(r).RUnlock() }
Lock 锁定 rw 以进行写入。如果该锁已被锁定以进行读取或写入,则 Lock 会阻塞,直到该锁可用为止。
func (rw *RWMutex) Lock() {
...
}
TryLock 尝试锁定 rw 进行写入并报告是否成功。
// Note that while correct uses of TryLock do exist, they are rare,
// and use of TryLock is often a sign of a deeper problem
// in a particular use of mutexes.
func (rw *RWMutex) TryLock() bool {
...
}
Unlock 释放写锁。
func (rw *RWMutex) Unlock() {
...
}
var (
rwMux = sync.RWMutex{}
data = make(map[int]int)
)
func read(key int) {
rwMux.RLock()
defer rwMux.RUnlock()
fmt.Printf("read %d -> %d\n", key, data[key])
}
func write(key, value int) {
rwMux.Lock()
defer rwMux.Unlock()
data[key] = value
fmt.Printf("write %d -> %d\n", key, value)
}
sync.Once 可以确保在程序运行过程中某个操作只会执行一次,即使被多个 goroutine 同时调用。一旦操作执行完成,后续的调用会立即返回而不再执行。
type Once struct {
done atomic.Uint32 // 指示动作是否被执行过
m Mutex // 互斥锁
}
对于一个Once,只有在第一次调用Once的Do方法时,才会执行传入的方法。
func (o *Once) Do(f func()) {
if o.done.Load() == 0 {
// Outlined slow-path to allow inlining of the fast-path.
o.doSlow(f)
}
}
特别注意:
func main() {
once := sync.Once{}
once.Do(func() {
once.Do(func() {
fmt.Println("HELLO WORLD")
})
})
}
Do 方法用于需要确保只运行一次的初始化操作。由于被执行的函数 f 是无参数的,因此有时候可能需要使用函数字面量(function literal)来捕获函数的参数,以便在 Do 方法中调用。
函数字面量是匿名函数的一种形式,可以在声明时直接定义函数体。通过函数字面量,我们可以将需要传递给函数的参数捕获到闭包中,然后在 Do 方法中调用这个函数。
func main() {
var once sync.Once
// 准备一个带参数的函数
initFunc := func(message string) {
fmt.Println("Initializing with message:", message)
}
// 使用函数字面量来捕获参数并传递给初始化函数
message := "Hello, World!"
once.Do(func() {
initFunc(message)
})
// 再次调用 Do 方法,函数将不会再次执行
once.Do(func() {
fmt.Println("This won't be executed again")
})
}
OnceFunc 返回一个仅调用 f 一次的函数。返回的函数可以被并发调用。如果 f 发生恐慌,则返回的函数将在每次调用时以相同的值发生恐慌。
func OnceFunc(f func()) func() {
...
}
func main() {
onceFunc := sync.OnceFunc(func() {
fmt.Println("Hello World")
})
go onceFunc()
go onceFunc()
go onceFunc()
go onceFunc()
time.Sleep(time.Second)
}
// 只会打印一次Hello World
OnceValue 返回一个仅调用 f 一次的函数,并返回 f 返回的值。返回的函数可以被并发调用。如果 f 发生恐慌,则返回的函数将在每次调用时以相同的值发生恐慌。
func OnceValue[T any](f func() T) func() T {
···
}
func main() {
onceValue := sync.OnceValue[string](func() string {
return time.Now().Format("20060102150405")
})
print := func() {
fmt.Println(onceValue())
}
go print()
go print()
go print()
time.Sleep(time.Second)
}
// 程序将打印三次同一个结果
OnceValues 返回一个仅调用 f 一次的函数,并返回 f 返回的值。返回的函数可以被并发调用。如果 f 发生恐慌,则返回的函数将在每次调用时以相同的值发生恐慌。
func OnceValues[T1, T2 any](f func() (T1, T2)) func() (T1, T2) {
...
}
该方法与OnceValue方法差不多,只是返回值的数量为两个而已。
WaitGroup用于等待一组goroutine执行结束。
要等待的主线程,通过调用Add方法来设定需要等待的goroutine数量。每个被等待的gorotine在执行完成时调用Done方法。等待的主线程将阻塞,直到所有被等待的goroutine执行完成。
type WaitGroup struct {
noCopy noCopy
state atomic.Uint64 // high 32 bits are counter, low 32 bits are waiter count.
sema uint32
}
Add方法向WaitGroup的计数器添加delta个等待数量。
func (wg *WaitGroup) Add(delta int) {
...
}
当计数器为0时,Add一个正数的操作应该在Wait操作之前。反之,Wait操作将不会阻塞。
当计数器大于0时,Add一个正数或负数可能发生在任何时候。
通常,这意味着对 Add 的调用应该在创建 goroutine 或其他要等待的事件的语句之前执行。如果重复使用 WaitGroup 来等待多个独立的事件集,则必须在所有先前的 Wait 调用返回后发生新的 Add 调用。
被等待的goroutine需要在执行完成后,调用Done方法来使WaitGroup的计数器减一。从源码可以看出,Done实际上是调用了Add方法。
func (wg *WaitGroup) Done() {
wg.Add(-1)
}
调用Wait方法的goroutine将被阻塞,直到WaitGroup的计数器变成0。
func (wg *WaitGroup) Wait() {
...
}
// This example fetches several URLs concurrently,
// using a WaitGroup to block until all the fetches are complete.
func ExampleWaitGroup() {
var wg sync.WaitGroup
var urls = []string{
"http://www.golang.org/",
"http://www.google.com/",
"http://www.example.com/",
}
for _, url := range urls {
// Increment the WaitGroup counter.
wg.Add(1)
// Launch a goroutine to fetch the URL.
go func(url string) {
// Decrement the counter when the goroutine completes.
defer wg.Done()
// Fetch the URL.
http.Get(url)
}(url)
}
// Wait for all HTTP fetches to complete.
wg.Wait()
}
Pool是一组可以单独保存和检索的临时对象的集合。
任何存储在 sync.Pool 中的项都可能在任何时候被自动移除,而没有任何通知。如果在移除操作发生时,sync.Pool 是存储项的唯一引用,那么这个项可能会被释放或回收。
在使用 sync.Pool 时,建议遵循以下最佳实践:
- 不依赖于池中对象的存在性,尽量避免在对象被移除后继续使用。
- 针对临时对象的重用和性能优化,而不是长期存储。
- 在池中存储的对象应该是无状态或者可重置状态的,以便于重复使用。
多个 goroutine 同时使用 Pool 是安全的。
sync.Pool 的作用是缓存已分配但未使用的项,以供以后重复使用,从而减轻垃圾回收器的压力。换句话说,它可以轻松构建高效、线程安全的空闲列表(free list)。但并不适用于所有类型的空闲列表。
池的其中一个适当用途是用来管理一组临时项,这些项在并发独立的客户端之间静默共享,并且有可能被重复使用。sync.Pool 提供了一种方式来在许多客户端之间分摊分配开销。
在 fmt 包中,sync.Pool 被用来维护一个动态大小的临时输出缓冲区存储。这个存储会根据负载情况进行扩展(当有很多 goroutine 在活跃地进行打印操作时),并在空闲时收缩。
对于生命周期短暂的对象,维护一个空闲列表可能会增加额外的开销,不适合使用 sync.Pool。
在这种情况下,最好让对象自己实现空闲列表的管理,以更好地控制资源的分配和释放。
type Pool struct {
noCopy noCopy
local unsafe.Pointer // local fixed-size per-P pool, actual type is [P]poolLocal
localSize uintptr // size of the local array
victim unsafe.Pointer // local from previous cycle
victimSize uintptr // size of victims array
// New optionally specifies a function to generate
// a value when Get would otherwise return nil.
// It may not be changed concurrently with calls to Get.
New func() any
}
New用于指定一个函数,用于在 Get 方法返回 nil 时生成一个值。同时指出,New 方法不应该在并发调用 Get 方法的情况下被同时修改。
在使用 sync.Pool 时,可以通过 New 方法指定一个函数,在 Get 方法无法返回值时,用于生成一个新的值。这样可以避免返回 nil,并且可以根据需要动态地生成新的值。需要注意的是,在调用 Get 方法的同时,不应该并发地修改 New 方法,以避免出现竞争条件和不确定的行为。
New 方法为 sync.Pool 提供了一种灵活的方式来生成值,从而确保在需要时能够获取到有效的值。
type Pool struct {
...
// New optionally specifies a function to generate
// a value when Get would otherwise return nil.
// It may not be changed concurrently with calls to Get.
New func() any
}
Put方法用于向池中添加以一个临时对象x。
func (p *Pool) Put(x any) {
...
}
Get 方法用于从池中获取一个项,并将其移除。需要注意的是,Get 方法可能会忽略池中的内容并返回一个新的项。此外,调用方不应该假设放入池中的值与 Get 返回的值之间存在任何关系,因为 Get 方法的行为是无法预测的。
func (p *Pool) Get() any {
...
}
var bufPool = sync.Pool{
New: func() any {
// The Pool's New function should generally only return pointer
// types, since a pointer can be put into the return interface
// value without an allocation:
return new(bytes.Buffer)
},
}
// timeNow is a fake version of time.Now for tests.
func timeNow() time.Time {
return time.Unix(1136214245, 0)
}
func Log(w io.Writer, key, val string) {
b := bufPool.Get().(*bytes.Buffer)
b.Reset()
// Replace this with time.Now() in a real logger.
b.WriteString(timeNow().UTC().Format(time.RFC3339))
b.WriteByte(' ')
b.WriteString(key)
b.WriteByte('=')
b.WriteString(val)
w.Write(b.Bytes())
bufPool.Put(b)
}
func ExamplePool() {
Log(os.Stdout, "path", "/search?q=flowers")
// Output: 2006-01-02T15:04:05Z path=/search?q=flowers
}
Map 类似于 Go 的 map[any]any,但可以安全地被多个 goroutine 并发使用,无需额外的锁定或协调。加载、存储和删除在分摊常量时间内运行。
在这两种情况下,与与单独的 Mutex 或 RWMutex 配对的 Go Map 相比,使用 Map 可以显着减少锁争用。
type Map struct {
mu Mutex
// read contains the portion of the map's contents that are safe for
// concurrent access (with or without mu held).
//
// The read field itself is always safe to load, but must only be stored with
// mu held.
//
// Entries stored in read may be updated concurrently without mu, but updating
// a previously-expunged entry requires that the entry be copied to the dirty
// map and unexpunged with mu held.
read atomic.Pointer[readOnly]
// dirty contains the portion of the map's contents that require mu to be
// held. To ensure that the dirty map can be promoted to the read map quickly,
// it also includes all of the non-expunged entries in the read map.
//
// Expunged entries are not stored in the dirty map. An expunged entry in the
// clean map must be unexpunged and added to the dirty map before a new value
// can be stored to it.
//
// If the dirty map is nil, the next write to the map will initialize it by
// making a shallow copy of the clean map, omitting stale entries.
dirty map[any]*entry
// misses counts the number of loads since the read map was last updated that
// needed to lock mu to determine whether the key was present.
//
// Once enough misses have occurred to cover the cost of copying the dirty
// map, the dirty map will be promoted to the read map (in the unamended
// state) and the next store to the map will make a new dirty copy.
misses int
}
Load方法返回存储在Map中的指定Key的值。
func (m *Map) Load(key any) (value any, ok bool) {
...
}
Store方法项map中设置指定的键值对
func (m *Map) Store(key, value any) {
_, _ = m.Swap(key, value)
}
LoadOrStore方法用于检查指定Key是否存在并返回Key对应的值。
func (m *Map) LoadOrStore(key, value any) (actual any, loaded bool) {
...
}
LoadAndDelete方法用于加载并删除指定的Key。
func (m *Map) LoadAndDelete(key any) (value any, loaded bool) {
...
}
删除Map中指定的Key。
func (m *Map) Delete(key any) {
m.LoadAndDelete(key)
}
Swap方法将指定的键值对存储到Map中,并返回原先存储的值。
func (m *Map) Swap(key, value any) (previous any, loaded bool) {
...
}
CompareAndSwap方法比较Map中指定Key的值是否等于old,如果等于old就设置新的值。
func (m *Map) CompareAndSwap(key, old, new any) bool {
...
}
CompareAndDelete方法比较Map中指定Key的值是否等于old,如果等于old就删除指定的键值对。
func (m *Map) CompareAndDelete(key, old any) (deleted bool) {
...
}
Range 为映射中存在的每个键和值依次调用 f 。如果 f 返回 false,则 range 停止迭代。
Range 不一定对应于 Map 内容的任何一致快照:不会多次访问任何键,但如果同时存储或删除任何键的值(包括通过 f),Range 可能会反映该键的任何映射Range 调用期间的任意点。 Range 不会阻塞接收器上的其他方法;甚至 f 本身也可以调用 m 上的任何方法。
func (m *Map) Range(f func(key, value any) bool) {
...
}
func main() {
var m sync.Map
var wg sync.WaitGroup
numRoutines := 5
// 并发写入
for i := 0; i < numRoutines; i++ {
wg.Add(1)
go func(i int) {
defer wg.Done()
m.Store(i, fmt.Sprintf("value%d", i))
}(i)
}
// 等待所有写入操作完成
wg.Wait()
// 并发读取
for i := 0; i < numRoutines; i++ {
wg.Add(1)
go func(i int) {
defer wg.Done()
if value, ok := m.Load(i); ok {
fmt.Printf("Key: %v, Value: %v\n", i, value)
}
}(i)
}
// 等待所有读取操作完成
wg.Wait()
}
func main() {
var m sync.Map
var wg sync.WaitGroup
numKeys := 5
// 初始化 sync.Map
for i := 0; i < numKeys; i++ {
m.Store(i, fmt.Sprintf("value%d", i))
}
// 并发删除
for i := 0; i < numKeys; i++ {
wg.Add(1)
go func(i int) {
defer wg.Done()
m.Delete(i)
fmt.Printf("Deleted key: %v\n", i)
}(i)
}
// 等待所有删除操作完成
wg.Wait()
}