Go语言同步与锁
本文最后更新于 2025-11-13,文章内容可能已经过时。
Go语言作为高并发编程的优秀语言,提供了丰富的同步原语来管理并发。下面我将详细介绍Go语言中常用的同步机制,包括适用场景和代码示例。
1. 互斥锁(Mutex)
原理
- sync.Mutex是最基本的互斥锁,保证同一时间只有一个Goroutine可以访问临界区
- 有Lock()和Unlock()两个方法
- 互斥锁不支持重入(同一个Goroutine不能多次获取同一个锁)
适用场景
- 读写操作频率相近的场景
- 需要保护共享资源不被并发修改的场景
- 读写不确定的场景(即读写次数没有明显区别)
代码示例
package main
import (
"fmt"
"sync"
"time"
)
var counter int = 0
var lock sync.Mutex
func main() {
var wg sync.WaitGroup
for i := 0; i < 5; i++ {
wg.Add(1)
go func(id int) {
defer wg.Done()
for j := 0; j < 10; j++ {
lock.Lock()
counter++
fmt.Printf("Goroutine %d sees counter: %d\n", id, counter)
lock.Unlock()
time.Sleep(time.Millisecond * 10)
}
}(i)
}
wg.Wait()
fmt.Println("Final counter:", counter)
}
注意事项
- 避免在锁内进行长时间操作(如I/O),否则会阻塞其他Goroutine
- 使用defer lock.Unlock()确保锁能正确释放
- 不要重复加锁,否则会导致死锁
2. 读写锁(RWMutex)
原理
- sync.RWMutex是读写互斥锁
- 允许多个Goroutine同时读取共享资源
- 但写操作是互斥的,即写操作会阻塞其他读和写操作
适用场景
- 读多写少的场景(如缓存系统、配置管理)
- 需要高并发读取,但写操作相对较少的场景
- 读操作频繁,写操作较少的场景
代码示例
package main
import (
"fmt"
"sync"
"time"
)
var data int = 0
var rwMutex sync.RWMutex
func readData(id int) {
for i := 0; i < 5; i++ {
rwMutex.RLock()
fmt.Printf("Reader %d: data is %d\n", id, data)
rwMutex.RUnlock()
time.Sleep(time.Millisecond * 50)
}
}
func writeData(id int) {
for i := 0; i < 3; i++ {
rwMutex.Lock()
data++
fmt.Printf("Writer %d: updated data to %d\n", id, data)
rwMutex.Unlock()
time.Sleep(time.Millisecond * 100)
}
}
func main() {
var wg sync.WaitGroup
// 启动多个读goroutine
for i := 0; i < 3; i++ {
wg.Add(1)
go func(id int) {
defer wg.Done()
readData(id)
}(i)
}
// 启动多个写goroutine
for i := 0; i < 2; i++ {
wg.Add(1)
go func(id int) {
defer wg.Done()
writeData(id)
}(i)
}
wg.Wait()
}
注意事项
- 读锁和写锁是互斥的,写操作会阻塞所有读操作
- 读锁可以重入(同一个Goroutine可以多次获取读锁)
- 不要长时间持有写锁,以免阻塞所有读操作
3. 条件变量(Cond)
原理
- sync.Cond用于在特定条件满足时唤醒等待的Goroutine
- 通常与互斥锁一起使用
- 有Wait()、Signal()和Broadcast() 方法
适用场景
- 生产者-消费者模型
- 等待特定条件满足的场景
- 需要协调多个Goroutine的场景
代码示例
package main
import (
"fmt"
"sync"
"time"
)
var items []int
var maxItems = 5
var mu sync.Mutex
var cond = sync.NewCond(&mu)
func producer() {
for i := 1; i <= 10; i++ {
mu.Lock()
for len(items) >= maxItems {
cond.Wait()
}
items = append(items, i)
fmt.Printf("Produced: %d\n", i)
mu.Unlock()
cond.Signal()
time.Sleep(time.Millisecond * 100)
}
}
func consumer() {
for {
mu.Lock()
for len(items) == 0 {
cond.Wait()
}
item := items[0]
items = items[1:]
fmt.Printf("Consumed: %d\n", item)
mu.Unlock()
cond.Signal()
time.Sleep(time.Millisecond * 200)
}
}
func main() {
go producer()
go consumer()
// 让程序运行一段时间
time.Sleep(time.Second * 5)
}
注意事项
- 条件变量必须与互斥锁一起使用
- 在调用Wait()之前必须持有锁
- 使用Signal()唤醒一个等待的Goroutine,使用Broadcast()唤醒所有等待的Goroutine
4. 原子操作(Atomic)
原理
- sync/atomic包提供了对基本数据类型的原子操作
- 保证对共享变量的读写操作是原子的
- 常用操作:AddInt32、LoadInt32、StoreInt32、SwapInt32、CompareAndSwapInt32
适用场景
- 简单计数器场景
- 需要高性能的共享变量操作
- 无需使用锁就能保证操作原子性的场景
代码示例
package main
import (
"fmt"
"sync"
"sync/atomic"
"time"
)
var counter int32 = 0
var wg sync.WaitGroup
func increment() {
for i := 0; i < 1000; i++ {
atomic.AddInt32(&counter, 1)
}
wg.Done()
}
func main() {
wg.Add(5)
for i := 0; i < 5; i++ {
go increment()
}
wg.Wait()
fmt.Println("Final counter:", counter)
}
注意事项
- 仅适用于基本数据类型
- 对于复杂数据结构,原子操作无法保证完整操作的原子性
- 适用于简单操作,对于复杂操作仍需使用锁
5. 等待组(WaitGroup)
原理
- sync.WaitGroup用于等待一组Goroutine完成
- 有Add()、Done()和Wait()三个方法
适用场景
- 需要等待多个Goroutine完成后再继续执行的场景
- 启动多个任务并等待它们完成
代码示例
package main
import (
"fmt"
"sync"
"time"
)
func worker(id int) {
fmt.Printf("Worker %d starting\n", id)
time.Sleep(time.Second)
fmt.Printf("Worker %d done\n", id)
}
func main() {
var wg sync.WaitGroup
for i := 1; i <= 5; i++ {
wg.Add(1)
go func(id int) {
defer wg.Done()
worker(id)
}(i)
}
wg.Wait()
fmt.Println("All workers done")
}
注意事项
- Add()必须在Start前调用
- Done()必须在Wait前调用
- 避免在Wait后再次调用Add或Done
6. Once
原理
- sync.Once确保某个操作只执行一次
- 有Do()方法
适用场景
- 需要确保初始化操作只执行一次的场景
- 单例模式
代码示例
package main
import (
"fmt"
"sync"
)
var once sync.Once
var initialized bool
func initResource() {
fmt.Println("Initializing resource")
initialized = true
}
func main() {
var wg sync.WaitGroup
for i := 0; i < 10; i++ {
wg.Add(1)
go func() {
defer wg.Done()
once.Do(initResource)
fmt.Println("Resource initialized:", initialized)
}()
}
wg.Wait()
}
注意事项
- 适用于需要初始化的单例模式
- 保证初始化操作只执行一次
- 适用于全局资源初始化
7. 对象池(Pool)
原理
- sync.Pool用于对象复用,减少内存分配和GC压力
- 有Get()和Put()方法
- 可以通过New函数指定创建新对象的函数
适用场景
- 需要频繁创建和销毁相同类型对象的场景
- 例如:HTTP请求处理中的context对象
- 高性能场景下减少内存分配
代码示例
package main
import (
"fmt"
"sync"
)
type Resource struct {
ID int
}
var pool = sync.Pool{
New: func() interface{} {
return &Resource{ID: 0}
},
}
func main() {
var wg sync.WaitGroup
for i := 0; i < 5; i++ {
wg.Add(1)
go func(id int) {
defer wg.Done()
// 从池中获取资源
res := pool.Get().(*Resource)
res.ID = id
fmt.Printf("Got resource %d\n", res.ID)
// 使用完后放回池中
pool.Put(res)
}(i)
}
wg.Wait()
}
注意事项
- 对象池中的对象可能在任何时候被回收
- 不要将需要持久化状态的对象放入池中
- 适用于临时对象,不适用于需要长期保存状态的对象
8. 并发安全Map(sync.Map)
原理
- sync.Map是Go 1.9引入的并发安全的Map
- 无需额外锁即可安全地进行并发操作
- 有Store、Load、Delete、Range方法
适用场景
- 需要并发安全的Map的场景
- 读多写少的场景
- 无需额外锁的简单Map操作
代码示例
package main
import (
"fmt"
"sync"
)
func main() {
var m sync.Map
// 写入操作
for i := 0; i < 10; i++ {
m.Store(i, fmt.Sprintf("Value %d", i))
}
// 读取操作
for i := 0; i < 10; i++ {
if val, ok := m.Load(i); ok {
fmt.Printf("Key %d: %s\n", i, val)
}
}
// 删除操作
m.Delete(5)
// 遍历操作
m.Range(func(key, value interface{}) bool {
fmt.Printf("Key: %v, Value: %v\n", key, value)
return true
})
// 读写并发示例
var wg sync.WaitGroup
for i := 10; i < 20; i++ {
wg.Add(1)
go func(key int) {
defer wg.Done()
m.Store(key, fmt.Sprintf("Value %d", key))
}(i)
}
wg.Wait()
fmt.Println("After concurrent writes:")
m.Range(func(key, value interface{}) bool {
fmt.Printf("Key: %v, Value: %v\n", key, value)
return true
})
}
注意事项
- 适用于读多写少的场景,写操作过多会影响性能
- 不需要额外的锁,但不能保证所有操作的原子性
- 适用于简单的键值对存储
总结
| 同步原语 | 适用场景 | 优点 | 缺点 |
|---|---|---|---|
| Mutex | 读写操作频率相近 | 简单易用 | 读操作多时性能差 |
| RWMutex | 读多写少 | 读操作并发性能高 | 写操作会阻塞所有读操作 |
| Cond | 生产者-消费者模型 | 精确控制等待条件 | 需要配合锁使用 |
| Atomic | 简单计数器 | 高性能,无需锁 | 仅适用于基本数据类型 |
| WaitGroup | 等待多个Goroutine完成 | 简单易用 | 不能用于任务调度 |
| Once | 初始化操作 | 确保只执行一次 | 仅适用于初始化 |
| Pool | 对象复用 | 减少内存分配 | 不能用于持久化对象 |
| sync.Map | 并发Map | 无需额外锁 | 写操作过多时性能下降 |
在实际开发中,应根据具体场景选择合适的同步机制。例如,如果需要高并发读取,应优先考虑RWMutex;如果只是简单的计数,atomic操作更为高效;对于需要对象复用的场景,sync.Pool是理想选择。
最后:并发编程的首要原则是避免共享状态。如果可能,尽量设计为不共享状态,这样可以避免使用同步机制,从而获得更好的性能和可维护性。
- 感谢你赐予我前进的力量
赞赏者名单
因为你们的支持让我意识到写文章的价值🙏
本文是原创文章,采用 CC BY-NC-ND 4.0 协议,完整转载请注明来自 软件从业者Hort
评论
匿名评论
隐私政策
你无需删除空行,直接评论以获取最佳展示效果

