Bootstrap

Go语言Sync包

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

语言中的并发编程。

;