在 Go 中,sync 包提供了基本的同步原件,用于在多个 goroutine 之间协调并发执行。sync 包是 Go 并发编程中非常重要的一部分,主要用于避免竞态条件、确保并发安全以及进行同步。
1. sync.Mutex(互斥锁)
Mutex 是最常用的同步原件,用于保证同一时刻只有一个 goroutine 可以访问共享资源。互斥锁通过 Lock 和 Unlock 方法来控制对临界区的访问。
1.1. 使用 sync.Mutex
package main
import (
"fmt"
"sync"
)
var (
counter int
mu sync.Mutex
)
func increment() {
mu.Lock()
counter++
mu.Unlock()
}
func main() {
var wg sync.WaitGroup
// 启动 10 个 goroutine,每个 goroutine 增加 counter
for i := 0; i < 10; i++ {
wg.Add(1)
go func() {
defer wg.Done()
increment()
}()
}
wg.Wait()
fmt.Println("Counter:", counter) // 输出: Counter: 10
}mu.Lock():加锁,进入临界区,其他 goroutine 无法再进入。mu.Unlock():解锁,允许其他 goroutine 访问临界区。
Mutex 保证了在一个时刻,只有一个 goroutine 可以访问共享资源 counter,从而避免了竞态条件。
1.2. sync.Mutex 的 Lock 和 Unlock 关系
Lock会阻塞当前 goroutine,直到锁可用。Unlock会解除阻塞,并允许其他 goroutine 获取锁。
注意:在使用 Mutex 时,务必确保在 Lock 后的代码执行完成后调用 Unlock,否则会导致死锁。通常使用 defer 来确保 Unlock 被执行。
2. sync.RWMutex(读写锁)
RWMutex 是一种改进的锁,支持多读单写的模式。多个 goroutine 可以同时读取共享资源,但写操作是独占的。在读操作较多、写操作较少的场景中,RWMutex 可以提高性能。
2.1. 使用 sync.RWMutex
package main
import (
"fmt"
"sync"
)
var (
counter int
mu sync.RWMutex
)
func read() int {
mu.RLock() // 加读锁
defer mu.RUnlock() // 解读锁
return counter
}
func write(val int) {
mu.Lock() // 加写锁
counter = val
mu.Unlock() // 解写锁
}
func main() {
var wg sync.WaitGroup
// 启动 10 个 goroutine 进行读操作
for i := 0; i < 10; i++ {
wg.Add(1)
go func() {
defer wg.Done()
fmt.Println(read())
}()
}
// 启动 1 个 goroutine 进行写操作
go func() {
write(100)
}()
wg.Wait()
}RLock:为读操作加锁,允许多个 goroutine 同时读取。RUnlock:解读锁。Lock:为写操作加锁,写操作时会阻止其他任何读操作和写操作。Unlock:解写锁。
3. sync.WaitGroup(等待组)
WaitGroup 用于等待一组 goroutine 执行完毕。当多个 goroutine 并发运行时,WaitGroup 可以用于等待它们完成后再继续执行后续操作。常用于等待一组任务的完成。
3.1. 使用 sync.WaitGroup
package main
import (
"fmt"
"sync"
)
func doWork(i int, wg *sync.WaitGroup) {
defer wg.Done() // 完成任务后调用 Done() 来减少计数
fmt.Printf("Working on task %d\n", i)
}
func main() {
var wg sync.WaitGroup
// 启动 5 个 goroutine
for i := 1; i <= 5; i++ {
wg.Add(1) // 增加等待的 goroutine 数量
go doWork(i, &wg)
}
// 等待所有的 goroutine 执行完毕
wg.Wait()
fmt.Println("All tasks completed")
}wg.Add(n):增加等待的 goroutine 数量。wg.Done():每个 goroutine 完成后调用Done,减少等待计数。wg.Wait():阻塞直到所有 goroutine 调用Done,等待计数归零。
4. sync.Once(单次操作)
sync.Once 用于确保某些操作只执行一次。无论调用多少次,Once 的操作只会执行一次,通常用于初始化工作(例如初始化全局变量)。
4.1. 使用 sync.Once
package main
import (
"fmt"
"sync"
)
var once sync.Once
func initOnce() {
fmt.Println("Initialization done")
}
func main() {
// 即使调用多次 initOnce,初始化操作也只会执行一次
once.Do(initOnce)
once.Do(initOnce)
once.Do(initOnce)
}once.Do(fn):确保fn只执行一次,即使调用多次,fn只会执行一次。
5. sync/atomic 包
sync/atomic 包提供了一些原子操作,可以在不使用锁的情况下保证对共享数据的安全操作。适用于计数器、标志位等需要频繁更新的场景,能够减少性能开销。
5.1. 原子操作示例
package main
import (
"fmt"
"sync"
"sync/atomic"
)
var counter int64
func increment() {
atomic.AddInt64(&counter, 1)
}
func main() {
var wg sync.WaitGroup
// 启动 10 个 goroutine 增加 counter
for i := 0; i < 10; i++ {
wg.Add(1)
go func() {
defer wg.Done()
increment()
}()
}
wg.Wait()
fmt.Println("Counter:", counter) // 输出: Counter: 10
}atomic.AddInt64(&counter, 1):原子地将counter增加 1,确保并发安全。
6. sync.Pool(对象池)
sync.Pool 用于临时对象的存储池,它可以帮助你减少内存分配和垃圾回收的开销。适用于那些频繁创建和销毁的对象,能有效提高性能。
6.1. 使用 sync.Pool
package main
import (
"fmt"
"sync"
)
var pool = sync.Pool{
New: func() interface{} {
return "New object"
},
}
func main() {
// 从池中获取对象
obj := pool.Get()
fmt.Println(obj)
// 将对象放回池中
pool.Put("Reused object")
// 再次获取对象,输出池中放回的对象
obj2 := pool.Get()
fmt.Println(obj2)
}pool.Get():从池中获取一个对象。pool.Put():将对象放回池中。
7. 总结
sync.Mutex:用于防止多个 goroutine 同时访问共享资源,通过加锁和解锁来实现同步。sync.RWMutex:读写锁,允许多个 goroutine 同时读取资源,但写操作是独占的。sync.WaitGroup:用于等待一组 goroutine 执行完毕,常用于并发任务的协调。sync.Once:保证某个操作只执行一次,适用于初始化等场景。sync/atomic:提供原子操作,减少锁的使用,提高性能,常用于计数器、标志位等。sync.Pool:提供临时对象池,用于减少内存分配和垃圾回收的开销,适用于频繁创建和销毁的对象。
Go 的 sync 包为并发编程提供了丰富的工具,可以帮助我们更高效、更安全地进行并发操作。