Go语言Sync包
1、sync.Mutex和sync.RWMutex
在处理 goroutine 时,确保它们不会同时访问资源是非常重要的,而 mutex 可以帮助我们做到这一点。
1.1 sync.Mutex
看看这个简单的例子,我没有使用互斥锁来保护我们的变量 a:
package main
import (
"fmt"
"time"
)
var a = 0
func Add() {
a++
}
func main() {
for i := 0; i < 500; i++ {
go Add()
}
time.Sleep(2 * time.Second)
// 595
fmt.Println(a)
}
此代码的结果是不可预测的,如果幸运的话,您可能会得到 500,但通常结果会小于 500。现在,让我们使用互斥
增强我们的 Add 函数:
package main
import (
"fmt"
"sync"
"time"
)
var a = 0
var mtx = sync.Mutex{}
func Add() {
mtx.Lock()
defer mtx.Unlock()
a++
}
func main() {
for i := 0; i < 500; i++ {
go Add()
}
time.Sleep(2 * time.Second)
// 500
fmt.Println(a)
}
现在,代码提供了预期的结果。但是使用 sync.RWMutex 呢?
1.2 sync.RWMutex
想象一下,您正在检查 a 变量,但其他 goroutines 也在调整它。您可能会得到过时的信息。那么,解决这个问题
的方法是什么?
让我们退后一步,使用我们的旧方法,将 sync.Mutex 添加到我们的 Get() 函数中:
package main
import (
"fmt"
"sync"
"time"
)
var a = 0
var mtx = sync.Mutex{}
func Add() {
mtx.Lock()
defer mtx.Unlock()
a++
}
func Get() int {
mtx.Lock()
defer mtx.Unlock()
return a
}
func main() {
for i := 0; i < 500; i++ {
go Add()
}
for i := 0; i < 5; i++ {
fmt.Println(Get())
}
time.Sleep(2 * time.Second)
// 500
fmt.Println(a)
}
# 输出
491
500
500
500
500
500
但这里的问题是,如果您的服务或程序调用 Get() 数百万次而只调用 Add() 几次,那么我们实际上是在浪费资源,
因为我们大部分时间甚至都没有修改它而将所有内容都锁定了。
这就是 sync.RWMutex 突然出现来拯救我们的一天,这个聪明的小工具旨在帮助我们处理同时读取和写入的情
况。
package main
import (
"fmt"
"sync"
"time"
)
var a = 0
var mtx = sync.RWMutex{}
func Add() {
mtx.Lock()
defer mtx.Unlock()
a++
}
func Look() {
mtx.RLock()
defer mtx.RUnlock()
fmt.Println(a)
}
func main() {
for i := 0; i < 500; i++ {
go Add()
}
for i := 0; i < 5; i++ {
Look()
}
time.Sleep(2 * time.Second)
// 500
fmt.Println(a)
}
# 输出
480
481
482
483
484
500
那么,RWMutex 有什么了不起的呢?好吧,它允许数百万次并发读取,同时确保一次只能进行一次写入。让我澄
清一下它是如何工作的:
- 写入时,读取被锁定。
- 读取时,写入被锁定。
- 多次读取不会相互锁定。
2、sync.Locker
Mutex 和 RWMutex 都实现了 sync.Locker 接口{},签名是这样的:
// A Locker represents an object that can be locked and unlocked.
type Locker interface {
Lock()
Unlock()
}
如果你想创建一个接受 Locker 的函数,你可以将这个函数与你的自定义 locker 或同步互斥锁一起使用:
package main
import (
"fmt"
"sync"
"time"
)
var a = 0
var mtx = sync.Mutex{}
func Add(lock sync.Locker) {
lock.Lock()
defer lock.Unlock()
a++
}
func main() {
for i := 0; i < 500; i++ {
go Add(&mtx)
}
time.Sleep(2 * time.Second)
// 500
fmt.Println(a)
}
3、sync.WaitGroup
您可能已经注意到我使用了 time.Sleep(2 * time.Second) 来等待所有 goroutine 完成,但老实说,这是一个非常
丑陋的解决方案。
这就是 sync.WaitGroup 出现的地方:
package main
import (
"fmt"
"sync"
)
var a = 0
var mtx = sync.Mutex{}
func Add() {
mtx.Lock()
defer mtx.Unlock()
a++
}
func main() {
wg := sync.WaitGroup{}
for i := 0; i < 500; i++ {
wg.Add(1)
go func() {
defer wg.Done()
Add()
}()
}
wg.Wait()
fmt.Println(a)
}
sync.WaitGroup 有 3 个主要方法:Add、Done 和 Wait。
首先是 Add(delta int):此方法将 WaitGroup 计数器增加 delta 的值。你通常会在生成 goroutine 之前调用它,表
示有一个额外的任务需要完成。
其他两种方法非常简单:
- 当一个 goroutine 结束它的任务时, Done 被调用。
- Wait 会阻塞调用者,直到 WaitGroup 计数器归零,这意味着所有派生的 goroutine 都已完成它们的任务。
4、sync.Once
假设您在一个包中有一个 CreateInstance() 函数,但您需要确保它在使用前已初始化。所以你在不同的地方多次
调用它,你的实现看起来像这样:
var i = 0
var _isInitialized = false
func CreateInstance() {
if _isInitialized {
return
}
i = GetISomewhere()
_isInitialized = true
}
但是如果有多个 goroutine 调用这个方法呢? i = GetISomeWhere 行会运行多次,即使您为了稳定性只希望它执
行一次。
您可以使用我们之前讨论过的互斥锁,但同步包提供了一种更方便的方法:sync.Once
package main
import (
"math/rand"
"sync"
)
var i = 0
var once = &sync.Once{}
func CreateInstance() {
once.Do(func() {
i = GetISomewhere()
})
}
func GetISomewhere() int {
return rand.Int()
}
func main() {
CreateInstance()
}
使用 sync.Once,你可以确保一个函数只执行一次,不管它被调用了多少次或者有多少 goroutines 同时调用它。
5、sync.Pool
5.1 简单使用
想象一下,你有一个池,里面有一堆你想反复使用的对象。这可以减轻垃圾收集器的一些压力,尤其是在创建和销
毁这些资源的成本很高的情况下。
所以,无论何时你需要一个对象,你都可以从池中取出它。当您使用完它时,您可以将它放回池中以备日后重复使
用。
package main
import (
"fmt"
"sync"
)
var pool = sync.Pool{
New: func() interface{} {
return 0
},
}
func main() {
pool.Put(1)
pool.Put(2)
pool.Put(3)
a := pool.Get().(int)
b := pool.Get().(int)
c := pool.Get().(int)
// 1 3 2
fmt.Println(a, b, c)
}
请记住,将对象放入池中的顺序不一定是它们出来的顺序,即使多次运行上述代码时顺序也是随机。
让我分享一些使用 sync.Pool 的技巧:
-
它非常适合长期存在并且有多个实例需要管理的对象,例如数据库连接(1000 个连接)、worker goroutine,
甚至缓冲区。
-
在将对象返回池之前始终重置对象的状态。这样,您可以避免任何无意的数据泄漏或奇怪的行为。
-
不要指望池中已经存在的对象,因为它们可能会意外释放。
5.2 介绍
在 Golang 中,sync.Pool 是一个非常有用的工具。它是用于存储和重用临时对象的池。在高并发的情况下,
sync.Pool 可以显著提高程序的性能,减少内存分配和垃圾回收的压力。本文将详细介绍 sync.Pool 的使用方法和
注意事项,希望能对广大 Golang 程序员有所帮助。
5.3 基本用法
sync.Pool 的基本用法非常简单。我们只需要创建一个 sync.Pool 对象,然后在需要使用临时对象的时候,从池中
取出对象即可。如果池中没有可用的对象,那么 Pool.Get() 方法会返回 nil。当我们用完对象之后,需要将其放回
池中,以便下次使用。
package main
import (
"fmt"
"sync"
)
func main() {
pool := &sync.Pool{
New: func() interface{} {
return "Hello, World!"
},
}
// 从池中取出对象
obj := pool.Get()
// 输出:Hello, World!
fmt.Println(obj)
// 将对象放回池中
pool.Put(obj)
// 再次从池中取出对象
obj = pool.Get()
// 输出:Hello, World!
fmt.Println(obj)
}
# 输出
Hello, World!
Hello, World!
在上面的示例中,我们创建了一个池,池中存储的是字符串 “Hello, World!”。我们首先从池中取出对象,然后将
其放回池中,最后再次取出对象。由于我们只创建了一个对象,所以两次输出的结果都是相同的。
5.4 高级用法
除了上面的基本用法之外,sync.Pool 还有一些高级用法,可以让我们更好地掌控池中对象的生命周期。下面我们
来逐一介绍这些用法。
5.4.1 生命周期
sync.Pool 中存储的对象并不会一直存在,它们的生命周期是由垃圾回收器控制的。如果一个对象在一定时间内没
有被使用,那么它就会被垃圾回收器回收。这个时间是不确定的,它取决于垃圾回收器的具体实现。
因此,我们不能依赖 sync.Pool 中的对象一直存在,必须在每次使用之前都检查对象是否为空。如果对象为空,那
么我们需要重新创建一个对象并放入池中。
5.4.2 GC 问题
由于 sync.Pool 中的对象是由垃圾回收器控制的,因此在使用 sync.Pool 时,需要注意避免对象被过早地回收。如
果我们在使用对象时没有及时将其放回池中,那么垃圾回收器可能会将对象回收,从而导致程序出现问题。
为了避免这种情况的发生,我们可以使用 sync.Pool 的 Finalizer 方法。Finalizer 方法可以在对象被回收之前执行
一些清理操作,从而保证对象在被回收之前能够被正确地处理。
下面是一个示例:
package main
import (
"fmt"
"runtime"
"sync"
"time"
)
type Foo struct {
Name string
}
func (f *Foo) Close() {
fmt.Printf("Closing Foo %s\n", f.Name)
}
func main() {
pool := &sync.Pool{
New: func() interface{} {
return &Foo{Name: "Bar"}
},
}
// 从池中取出对象
obj := pool.Get().(*Foo)
fmt.Println(obj.Name) // 输出:Bar
// 将对象放回池中
pool.Put(obj)
// 等待 1 秒钟
time.Sleep(time.Second)
// 再次从池中取出对象
obj = pool.Get().(*Foo)
fmt.Println(obj.Name) // 输出:Bar
// 使用 Finalizer 方法
runtime.SetFinalizer(obj, func(f *Foo) {
f.Close()
})
// 等待 1 秒钟
time.Sleep(time.Second)
}
# 输出
Bar
Bar
在上面的示例中,我们创建了一个 Foo 对象,并将其放入 sync.Pool 中。我们在使用对象之前,等待了 1 秒钟,
从而模拟对象被长时间占用的情况。然后我们再次从池中取出对象,并使用 Finalizer 方法为其设置一个回收方
法。该回收方法会在对象被回收之前执行,从而保证对象能够被正确地处理。
5.4.3 并发安全性
sync.Pool 对象本身是并发安全的。多个 goroutine 可以同时访问同一个 sync.Pool 对象,并且不需要额外的锁来
保证并发安全性。这是因为 sync.Pool 内部使用了 sync.Mutex 来保证并发安全性。
但是,需要注意的是,由于 sync.Pool 中存储的对象是共享的,因此我们需要在使用对象时进行一些额外的同步操
作,以避免出现竞态条件。例如,如果我们从池中取出一个对象,然后对其进行修改,那么其他 goroutine 可能
会同时访问到同一个对象,从而导致数据竞争。
下面是一个示例:
package main
import (
"fmt"
"sync"
)
type Counter struct {
mu sync.Mutex
count int
}
func (c *Counter) Add(n int) {
c.mu.Lock()
defer c.mu.Unlock()
c.count += n
}
func (c *Counter) Value() int {
c.mu.Lock()
defer c.mu.Unlock()
return c.count
}
func main() {
pool := &sync.Pool{
New: func() interface{} {
return &Counter{}
},
}
// 从池中取出对象
counter := pool.Get().(*Counter)
// 对对象进行修改
counter.Add(1)
// 将对象放回池中
pool.Put(counter)
// 再次从池中取出对象
counter = pool.Get().(*Counter)
// 对对象进行修改
counter.Add(1)
// 输出对象的值
fmt.Println(counter.Value()) // 输出:2
}
# 输出
2
在上面的示例中,我们创建了一个 Counter 对象,并将其放入 sync.Pool 中。我们首先从池中取出对象,并对其
进行修改。然后将对象放回池中,再次取出对象,并对其进行修改。最后输出对象的值。由于我们对同一个对象进
行了两次修改,因此输出的结果是 2。
6、sync.Map
当您同时使用 map 时,有点像使用 RWMutex。您可以同时进行多次读取,但不能进行多次读写或写入。如果存
在冲突,您的服务将崩溃而不是覆盖数据或导致意外行为。
这就是 sync.Map 派上用场的地方,因为它可以帮助我们避免这个问题。让我们仔细看看 sync.Map 给我们提供什
么:
- CompareAndDelete (go 1.20):如果值匹配则删除键的条目;如果不存在值或旧值为 nil,则返回 false。
- CompareAndSwap(go 1.20):如果新旧值匹配,则交换一个键,只要确保旧值是可比较的。
- Swap (go 1.20):交换键的值并返回旧值(如果存在)。
- LoadOrStore:获取当前键值或保存并返回提供的值(如果不存在)
- Range (f func(key, value any):遍历映射,将函数 f 应用于每个键值对。如果 f 说返回 false,它会停止。
- Store
- Delete
- Load
- LoadAndDelete
我们为什么不使用带有 Mutex 的常规 map 呢?
我通常选择带有 RWMutex 的 map,但在某些情况下认识到 sync.Map 的强大功能很重要。那么,它真正发光的
地方在哪里呢?
如果您有许多 goroutines 访问 map 中的单独键,则具有单个互斥锁的常规 map 可能会导致争用,因为它仅针对
单个写操作锁定整个 map。
另一方面,sync.Map 使用更完善的锁定机制,有助于最大限度地减少此类场景中的争用。
package main
import (
"fmt"
"sync"
)
func main() {
var m sync.Map
// 1. 写入
m.Store("aa", 18)
m.Store("bb", 20)
// 2. 读取
age, _ := m.Load("aa")
fmt.Println(age.(int))
// 3. 遍历
m.Range(func(key, value interface{}) bool {
name := key.(string)
age := value.(int)
fmt.Println(name, age)
return true
})
// 4. 删除
m.Delete("aa")
age, ok := m.Load("aa")
fmt.Println(age, ok)
// 5. 读取或写入
// 这个key已经存在,因此写入不成功,并且读出原值
m.LoadOrStore("bb", 100)
age, _ = m.Load("bb")
fmt.Println(age)
}
# 程序输出
18
aa 18
bb 20
<nil> false
20
7、sync.Cond
在并发编程中,条件变量是一种常用的线程间协作机制,它可以让一个或多个线程等待某个条件的满足,从而实现
线程间的同步和通信。在 Go 语言中,sync 包提供了 Cond 类型来支持条件变量的使用。
sync.Cond 是基于互斥锁/读写锁实现的条件变量,用来协调想要访问共享资源的那些 Goroutine。当共享资源状
态发生变化时,sync.Cond 可以用来通知等待条件发生而阻塞的 Goroutine。
sync.Cond 基于互斥锁/读写锁,那它和互斥锁有什么区别呢?
互斥锁 sync.Mutex 通常用来保护共享的临界资源,条件变量 sync.Cond 用来协调想要访问共享资源的
Goroutine。当共享资源的状态发生变化时,sync.Cond 可以用来通知被阻塞的 Goroutine。
下面将深入探讨 sync.Cond 的使用方法和注意事项,帮助你更好地理解 Go 语言中的并发编程。
7.1 简单案例
将 sync.Cond 视为支持多个 goroutine 等待和相互交互的条件变量。为了更好地理解,让我们看看如何使用它。
首先,我们需要创建带有 Locker 的 sync.Cond:
var mtx sync.Mutex
var cond = sync.NewCond(&mtx)
goroutine 调用 cond.Wait 并等待来自其他地方的信号以继续执行:
func dummyGoroutine(id int) {
cond.L.Lock()
defer cond.L.Unlock()
fmt.Printf("Goroutine %d is waiting...\n", id)
cond.Wait()
fmt.Printf("Goroutine %d received the signal.\n", id)
}
然后,另一个 goroutine(就像主 goroutine)调用 cond.Signal(),让我们等待的 goroutine 继续:
func main() {
go dummyGoroutine(1)
time.Sleep(1 * time.Second)
fmt.Println("Sending signal...")
cond.Signal()
time.Sleep(1 * time.Second)
}
总代码:
package main
import (
"fmt"
"sync"
"time"
)
var mtx sync.Mutex
var cond = sync.NewCond(&mtx)
func dummyGoroutine(id int) {
cond.L.Lock()
defer cond.L.Unlock()
fmt.Printf("Goroutine %d is waiting...\n", id)
cond.Wait()
fmt.Printf("Goroutine %d received the signal.\n", id)
}
func main() {
go dummyGoroutine(1)
time.Sleep(1 * time.Second)
fmt.Println("Sending signal...")
cond.Signal()
time.Sleep(1 * time.Second)
}
结果如下所示:
# 输出
Goroutine 1 is waiting...
Sending signal...
Goroutine 1 received the signal.
如果有多个 goroutines 在等待我们的信号怎么办? 这就是我们可以使用广播的时候:
package main
import (
"fmt"
"sync"
"time"
)
var mtx sync.Mutex
var cond = sync.NewCond(&mtx)
func dummyGoroutine(id int) {
cond.L.Lock()
defer cond.L.Unlock()
fmt.Printf("Goroutine %d is waiting...\n", id)
cond.Wait()
fmt.Printf("Goroutine %d received the signal.\n", id)
}
func main() {
go dummyGoroutine(1)
go dummyGoroutine(2)
time.Sleep(1 * time.Second)
// broadcast to all goroutines
cond.Broadcast()
time.Sleep(1 * time.Second)
}
结果如下所示:
# 输出
Goroutine 1 is waiting...
Goroutine 2 is waiting...
Goroutine 2 received the signal.
Goroutine 1 received the signal.
7.2 实现原理
条件变量的实现原理基于互斥锁和 goroutine 队列。
假设有一个条件变量 cond,初始时它没有被触发。当一个 goroutine 调用 cond.Wait() 方法时,它会加锁并将自
己加入到 cond 的 goroutine 队列中。接着,它会解锁并进入睡眠状态,等待被唤醒。
当另一个 goroutine 调用 cond.Signal() 或者 cond.Broadcast() 方法时,它会重新加锁,并从 cond 的 goroutine
队列中选择一个 goroutine 唤醒。被唤醒的 goroutine 会重新加锁,然后继续执行。
需要注意的是,被唤醒的 goroutine 并不会立即执行,它会等待重新获得锁之后才会继续执行。
7.3 Cond 类型
Cond 类型是 Go 语言中的条件变量类型,它的定义如下:
type Cond struct {
// contains filtered or unexported fields
}
Cond 类型包含了一些私有字段,我们无法直接访问它们。但是,sync 包提供了一些方法来操作 Cond 类型的实
例。
7.4 NewCond创建实例
sync.Cond 对象需要依赖一个 sync.Mutex 或 sync.RWMutex 对象来进行同步和互斥操作。我们可以使用
sync.NewCond 方法来创建一个新的 sync.Cond 对象,该方法接受一个 Mutex 或 RWMutex 对象作为参数,返回
一个对应的条件变量对象。
func NewCond(l Locker) *Cond
7.5 Wait 方法
sync.Cond 提供了 Wait 方法来等待条件变量的信号。Wait 方法需要在持有 Mutex 或 RWMutex 的情况下进行调
用,否则会抛出 panic 异常。
Wait 方法是 Cond 类型的核心方法之一,它用于等待条件变量的满足。Wait 方法的定义如下:
func (c *Cond) Wait()
Wait 方法将当前 goroutine 暂停,等待条件变量的信号。在等待过程中,Mutex 或 RWMutex 将被释放,其他
goroutine 可以获取锁并修改共享变量,但是当前 goroutine 仍然保持在等待队列中,直到收到唤醒信号。当
Wait 方法返回时,Mutex 或 RWMutex 会自动重新被锁定。
Wait 方法会阻塞当前的 goroutine,直到条件变量满足。在调用 Wait 方法之前,我们需要先获取锁,以确保条件
变量的正确使用。例如:
package main
import (
"fmt"
"sync"
"time"
)
var mu sync.Mutex
var cond = sync.NewCond(&mu)
func main() {
go func() {
time.Sleep(time.Second)
mu.Lock()
cond.Signal()
mu.Unlock()
}()
mu.Lock()
cond.Wait()
fmt.Println("condition satisfied")
mu.Unlock()
}
# 输出
condition satisfied
在上面的例子中,我们创建了一个 Mutex 类型的实例 mu 和一个 Cond 类型的实例 cond。在主 goroutine 中,
我们先获取了锁 mu,然后调用 cond.Wait() 方法等待条件变量的满足。在另一个 goroutine 中,我们等待 1 秒钟
后获取了锁 mu,然后调用 cond.Signal() 方法向等待的 goroutine 发送信号,表示条件变量已经满足。最后,我
们释放锁 mu。
需要注意的是,Wait 方法会自动释放锁,以便其他 goroutine 可以获取锁并修改条件变量。当 Wait 方法返回时,
它会重新获取锁,以便继续执行后续的代码。
7.6 Signal 唤醒一个协程
Signal 方法用于唤醒等待队列中的一个 goroutine,使其继续执行。在调用 Signal 方法之前,必须先获得 Mutex
或 RWMutex 的锁。
Signal 方法用于向等待的 goroutine 发送信号,表示条件变量已经满足。其定义如下:
func (c *Cond) Signal()
Signal 方法会选择一个等待的 goroutine 并唤醒它。如果没有等待的 goroutine,Signal 方法不会做任何事情。
7.7 Broadcast 广播唤醒所有
Broadcast 方法用于唤醒等待队列中的所有 goroutine,使它们继续执行。在调用 Broadcast 方法之前,必须先获
得 Mutex 或 RWMutex 的锁。
Broadcast 方法用于向所有等待的 goroutine 发送信号,表示条件变量已经满足。其定义如下:
func (c *Cond) Broadcast()
Broadcast 方法会唤醒所有等待的 goroutine。如果没有等待的 goroutine,Broadcast 方法不会做任何事情。
需要注意的是,Signal 和 Broadcast 方法只有在获取锁之后才能调用。否则,会导致 panic。
7.8 示例1
下面是一个使用 Cond 类型实现生产者-消费者模型的示例:
package main
import (
"fmt"
"math/rand"
"sync"
"time"
)
type Queue struct {
items []int
cond *sync.Cond
}
func NewQueue() *Queue {
q := &Queue{cond: sync.NewCond(&sync.Mutex{})}
go func() {
for {
time.Sleep(time.Second)
q.cond.L.Lock()
q.items = append(q.items, rand.Intn(100))
q.cond.Signal()
q.cond.L.Unlock()
}
}()
return q
}
func (q *Queue) Get() int {
q.cond.L.Lock()
for len(q.items) == 0 {
q.cond.Wait()
}
item := q.items[0]
q.items = q.items[1:]
q.cond.L.Unlock()
return item
}
func main() {
q := NewQueue()
for i := 0; i < 10; i++ {
fmt.Println(q.Get())
}
}
# 输出
81
87
47
59
81
18
25
40
56
0
在上面的例子中,我们定义了一个 Queue 类型,它包含一个 int 类型的切片 items 和一个 Cond 类型的实例
cond。在 NewQueue 函数中,我们启动了一个 goroutine,每秒钟向 items 中添加一个随机数,并调用
cond.Signal() 方法通知等待的 goroutine。在 Get 方法中,我们使用 cond.Wait() 方法等待 items 不为空,并返回
第一个元素。
7.9 示例2
package main
import (
"log"
"sync"
"time"
)
var done = false
func read(name string, c *sync.Cond) {
c.L.Lock()
for !done {
c.Wait()
}
log.Println(name, "starts reading")
c.L.Unlock()
}
func write(name string, c *sync.Cond) {
log.Println(name, "starts writing")
time.Sleep(time.Second)
c.L.Lock()
done = true
c.L.Unlock()
log.Println(name, "wakes all")
c.Broadcast()
}
func main() {
cond := sync.NewCond(&sync.Mutex{})
go read("reader1", cond)
go read("reader2", cond)
go read("reader3", cond)
write("writer", cond)
time.Sleep(time.Second * 3)
}
# 输出
2023/07/03 16:02:08 writer starts writing
2023/07/03 16:02:09 writer wakes all
2023/07/03 16:02:09 reader3 starts reading
2023/07/03 16:02:09 reader1 starts reading
2023/07/03 16:02:09 reader2 starts reading
-
done 即互斥锁需要保护的条件变量。
-
read() 调用 Wait() 等待通知,直到 done 为 true。
-
write() 接收数据,接收完成后,将 done 置为 true,调用 Broadcast() 通知所有等待的协程。
-
write() 中的暂停了 1s,一方面是模拟耗时,另一方面是确保前面的 3 个 read 协程都执行到 Wait(),处于等
待状态。main 函数最后暂停了 3s,确保所有操作执行完毕。
7.10 注意事项
在使用 Cond 类型时,需要注意以下几点:
-
在调用 Wait 方法之前,必须先获取锁。否则,会导致 panic。
-
Wait 方法会自动释放锁,以便其他 goroutine 可以获取锁并修改条件变量。当 Wait 方法返回时,它会重新获
取锁,以便继续执行后续的代码。
-
Signal 和 Broadcast 方法只有在获取锁之后才能调用。否则,会导致 panic。
-
Signal 方法会选择一个等待的 goroutine 并唤醒它。如果没有等待的 goroutine,Signal 方法不会做任何事
情。
-
Broadcast 方法会唤醒所有等待的 goroutine。如果没有等待的 goroutine,Broadcast 方法不会做任何事
情。
-
在使用 sync.Cond 前,一定要先创建一个互斥锁。
-
在调用 Wait 方法前,一定要先获取互斥锁,否则会导致死锁。
-
在调用 Wait 方法后,当前 goroutine 会被阻塞,直到被唤醒。
-
在调用 Signal 或 Broadcast 方法后,等待队列中的一个或多个 goroutine 会被唤醒,但不会立即获取互斥
锁。因此,在使用 Signal 或 Broadcast 方法时,一定要保证唤醒的 goroutine 不会互相竞争同一个资源。
-
在调用 Signal 或 Broadcast 方法后,一定要释放互斥锁,否则被唤醒的 goroutine 无法获取到互斥锁,仍然
会被阻塞。
-
在使用 sync.Cond 时,一定要注意竞争条件和数据同步的问题,确保程序的正确性和稳定性。
7.11 结语
在上面我们深入探讨了 sync.Cond 类型的使用方法和注意事项。我们学习了 Wait、Signal 和 Broadcast 方法的使
用,并通过一个生产者-消费者模型的示例来演示了 Cond 类型的实际应用。希望本文能够帮助你更好地理解 Go
语言中的并发编程。