Lock
在 Go 中,锁(Lock)用于管理并发访问共享资源,防止多个 goroutine 同时修改资源,确保程序的正确性。Go 提供了几种常见的锁机制,如 互斥锁(Mutex) 和 读写锁(RWMutex),它们位于 sync 包中。
1. 互斥锁(Mutex)
互斥锁(Mutex)是最常见的同步机制之一,保证同一时刻只有一个 goroutine 能访问共享资源。sync.Mutex 提供了两个方法:
Lock():用于加锁。Unlock():用于解锁。
1.1 使用 sync.Mutex
下面是一个使用 sync.Mutex 的示例,展示如何保护一个共享变量,以避免并发访问时发生数据竞争:
package main
import (
"fmt"
"sync"
)
var (
counter int
mu sync.Mutex
)
func increment() {
mu.Lock() // 加锁
defer mu.Unlock() // 解锁
// 修改共享资源
counter++
}
func main() {
var wg sync.WaitGroup
// 启动多个 goroutine 来并发修改共享资源
for i := 0; i < 1000; i++ {
wg.Add(1)
go func() {
defer wg.Done()
increment()
}()
}
wg.Wait() // 等待所有 goroutine 完成
fmt.Println("Counter:", counter) // 输出最终的结果
}在上面的示例中,mu.Lock() 和 mu.Unlock() 确保每次只有一个 goroutine 可以访问 counter,从而避免了竞态条件。
2. 读写锁(RWMutex)
sync.RWMutex 是另一种常用的锁类型,它允许多个 goroutine 同时读取共享资源,但在写操作时,只允许一个 goroutine 访问资源。这对于读取频繁、写入较少的情况非常有用,可以提高性能。
sync.RWMutex 提供了以下方法:
RLock():获取读锁。RUnlock():释放读锁。Lock():获取写锁。Unlock():释放写锁。
2.1 使用 sync.RWMutex
下面是一个示例,展示如何使用 sync.RWMutex 来进行并发读写控制:
package main
import (
"fmt"
"sync"
)
var (
data map[string]string
mu sync.RWMutex
)
func readData(key string) string {
mu.RLock() // 获取读锁
defer mu.RUnlock() // 释放读锁
return data[key]
}
func writeData(key, value string) {
mu.Lock() // 获取写锁
defer mu.Unlock() // 释放写锁
data[key] = value
}
func main() {
data = make(map[string]string)
var wg sync.WaitGroup
// 启动多个读 goroutine
for i := 0; i < 5; i++ {
wg.Add(1)
go func(i int) {
defer wg.Done()
fmt.Println("Reading:", readData("key"))
}(i)
}
// 启动一个写 goroutine
wg.Add(1)
go func() {
defer wg.Done()
writeData("key", "value")
}()
wg.Wait() // 等待所有 goroutine 完成
}在此示例中,多个 goroutine 可以并发执行读取操作,但在写操作时,mu.Lock() 会阻止任何读写操作,确保在修改数据时没有其他 goroutine 访问该数据。
3. 条件变量(Cond)
sync.Cond 是另一种同步原语,它允许 goroutine 等待某个条件满足时再继续执行。条件变量经常和互斥锁一起使用,以便在某个条件成立时通知其他等待的 goroutine。
sync.Cond 提供了以下方法:
Wait():等待通知。Signal():唤醒一个等待的 goroutine。Broadcast():唤醒所有等待的 goroutine。
3.1 使用 sync.Cond
下面是一个使用条件变量的示例:
package main
import (
"fmt"
"sync"
)
var (
data int
cond = sync.NewCond(&sync.Mutex{})
)
func produce() {
// 模拟生产数据
data = 10
cond.Signal() // 唤醒等待的消费者
}
func consume() {
cond.L.Lock() // 获取锁
defer cond.L.Unlock()
// 等待直到生产者提供数据
for data == 0 {
cond.Wait()
}
// 消费数据
fmt.Println("Consumed:", data)
data = 0
}
func main() {
var wg sync.WaitGroup
// 启动多个消费者
for i := 0; i < 2; i++ {
wg.Add(1)
go func() {
defer wg.Done()
consume()
}()
}
// 启动生产者
wg.Add(1)
go func() {
defer wg.Done()
produce()
}()
wg.Wait() // 等待所有 goroutine 完成
}在这个例子中,消费者会在没有数据时等待,而生产者在生成数据后会唤醒消费者。
4. 原子操作(Atomic)
Go 还提供了一些原子操作,通过 sync/atomic 包来执行一些基本的、不可中断的操作。原子操作不需要显式地加锁,是一种轻量级的同步机制。
常用的原子操作方法包括:
atomic.AddInt32()atomic.CompareAndSwapInt32()atomic.LoadInt32()atomic.StoreInt32()
4.1 使用 sync/atomic 实现原子操作
package main
import (
"fmt"
"sync"
"sync/atomic"
)
var counter int32
func increment() {
atomic.AddInt32(&counter, 1)
}
func main() {
var wg sync.WaitGroup
// 启动多个 goroutine 来并发修改共享资源
for i := 0; i < 1000; i++ {
wg.Add(1)
go func() {
defer wg.Done()
increment()
}()
}
wg.Wait() // 等待所有 goroutine 完成
fmt.Println("Counter:", counter) // 输出最终的结果
}在这个示例中,atomic.AddInt32 被用来对 counter 进行原子加法操作,而不需要显式地使用互斥锁。
5. 总结
Go 提供了多种同步机制来确保并发程序的正确性:
- 互斥锁(Mutex):最常用的锁,保证同一时刻只有一个 goroutine 可以访问共享资源。
- 读写锁(RWMutex):适用于读多写少的场景,允许多个 goroutine 同时读取共享资源,但写操作时会加锁。
- 条件变量(Cond):用于在某些条件下同步 goroutine 的执行,适用于生产者消费者问题。
- 原子操作(Atomic):提供轻量级的原子操作,用于避免加锁的高开销。
根据具体的并发场景,选择合适的锁类型可以有效避免数据竞争并提高程序的性能。