Go语言中的sync包:同步原语详解
大家好,欢迎来到今天的Go语言技术讲座!今天我们要聊的是Go语言中一个非常重要的工具——sync
包。如果你是一个喜欢写并发程序的开发者,那么这个包简直就是你的“救星”。它就像是一位严格的交通警察,确保你的协程(goroutines)在繁忙的道路上井然有序地运行。
为了让这次讲座更加轻松愉快,我会用一些诙谐的语言和实际的例子来解释这些概念。别担心,代码会很多,表格也会有,让你看得更清楚明白!
什么是sync
包?
首先,我们来简单介绍一下sync
包。sync
包是Go语言标准库的一部分,提供了许多用于实现线程安全(或者说是协程安全)的工具。它的主要目的是帮助我们在多协程环境下管理共享资源,避免出现竞态条件(race condition)或其他并发问题。
换句话说,sync
包就是用来让多个协程“和平共处”的工具集合。
同步原语有哪些?
接下来,我们来看看sync
包中提供的几种常见的同步原语:
- Mutex(互斥锁)
- RWMutex(读写锁)
- WaitGroup(等待组)
- Once(一次性执行)
- Cond(条件变量)
下面我们将逐一讲解这些原语,并通过代码示例说明它们的作用。
1. Mutex(互斥锁)
Mutex
是最基本的同步工具之一,用于保护共享资源,防止多个协程同时访问。它的工作原理很简单:如果一个协程持有锁,其他协程就必须等待,直到锁被释放。
示例代码
package main
import (
"fmt"
"sync"
)
func main() {
var mu sync.Mutex
count := 0
var wg sync.WaitGroup
for i := 0; i < 10; i++ {
wg.Add(1)
go func() {
defer wg.Done()
mu.Lock() // 加锁
count++
fmt.Println("Current count:", count)
mu.Unlock() // 解锁
}()
}
wg.Wait()
fmt.Println("Final count:", count)
}
运行结果
每次运行的结果都应该是:
Final count: 10
注意事项
- 如果忘记调用
Unlock()
,会导致死锁。 Lock()
和Unlock()
必须成对使用。
2. RWMutex(读写锁)
RWMutex
是Mutex
的升级版,允许多个协程同时读取共享资源,但只允许一个协程写入。这在读操作远多于写操作的场景下非常有用。
示例代码
package main
import (
"fmt"
"sync"
)
func main() {
var rwMu sync.RWMutex
data := make(map[string]string)
var wg sync.WaitGroup
for i := 0; i < 5; i++ {
wg.Add(1)
go func(i int) {
defer wg.Done()
rwMu.RLock() // 加读锁
if value, ok := data["key"]; ok {
fmt.Println("Read:", value)
} else {
fmt.Println("Key not found")
}
rwMu.RUnlock() // 解读锁
}(i)
}
wg.Add(1)
go func() {
defer wg.Done()
rwMu.Lock() // 加写锁
data["key"] = "value"
fmt.Println("Write: key -> value")
rwMu.Unlock() // 解写锁
}()
wg.Wait()
}
注意事项
- 写锁会阻塞所有读锁和写锁。
- 多个读锁可以同时存在。
3. WaitGroup(等待组)
WaitGroup
用于等待一组协程完成任务后再继续执行。它非常适合需要协调多个协程的场景。
示例代码
package main
import (
"fmt"
"sync"
"time"
)
func worker(id int, wg *sync.WaitGroup) {
defer wg.Done() // 标记任务完成
fmt.Printf("Worker %d startedn", id)
time.Sleep(time.Second)
fmt.Printf("Worker %d finishedn", id)
}
func main() {
var wg sync.WaitGroup
for i := 1; i <= 5; i++ {
wg.Add(1) // 增加计数器
go worker(i, &wg)
}
wg.Wait() // 等待所有协程完成
fmt.Println("All workers have finished")
}
注意事项
- 必须在启动协程之前调用
Add()
。 - 使用
Done()
标记协程完成。
4. Once(一次性执行)
Once
确保某个操作只执行一次,即使有多个协程同时尝试执行该操作。
示例代码
package main
import (
"fmt"
"sync"
)
func main() {
var once sync.Once
value := 0
for i := 0; i < 5; i++ {
go func() {
once.Do(func() {
value = 42
fmt.Println("Value initialized to 42")
})
fmt.Println("Value read:", value)
}()
}
time.Sleep(time.Second) // 等待协程完成
}
注意事项
Do()
中的函数只会被执行一次。- 即使有多个协程调用
Do()
,也只会有一个协程执行其中的代码。
5. Cond(条件变量)
Cond
用于实现复杂的同步逻辑,允许协程等待某些条件满足后再继续执行。
示例代码
package main
import (
"fmt"
"sync"
)
func main() {
var mu sync.Mutex
cond := sync.NewCond(&mu)
var ready bool
go func() {
mu.Lock()
for !ready {
cond.Wait() // 等待条件满足
}
fmt.Println("Condition met, continuing...")
mu.Unlock()
}()
mu.Lock()
ready = true
cond.Signal() // 通知条件已满足
mu.Unlock()
}
注意事项
- 必须与
Mutex
或RWMutex
配合使用。 - 使用
Signal()
通知单个协程,使用Broadcast()
通知所有协程。
总结
为了让大家更好地理解这些同步原语的特点,我做了一个简单的对比表:
同步原语 | 功能描述 | 场景 |
---|---|---|
Mutex | 保护共享资源,防止并发访问 | 修改共享数据时 |
RWMutex | 支持并发读取,限制写入 | 读多写少的场景 |
WaitGroup | 等待一组协程完成 | 协调多个协程 |
Once | 确保某段代码只执行一次 | 初始化全局变量 |
Cond | 实现复杂的同步逻辑 | 需要等待条件满足 |
最后,引用一下Go官方文档的一句话:“Concurrency is not parallelism.” 并发不是并行,但它确实能让我们的程序更高效、更优雅!
希望今天的讲座对你有所帮助!如果有任何问题,欢迎随时提问!