📚 原创系列: “Go语言学习系列”
🔄 转载说明: 本文最初发布于"Gopher部落"微信公众号,经原作者授权转载。
🔗 关注原创: 欢迎扫描文末二维码,关注"Gopher部落"微信公众号获取第一手Go技术文章。
📑 Go语言学习系列导航
🚀 第三阶段:进阶篇本文是【Go语言学习系列】的第31篇,当前位于第三阶段(进阶篇)
- 并发编程(一):goroutine基础
- 并发编程(二):channel基础
- 并发编程(三):select语句
- 并发编程(四):sync包 👈 当前位置
- 并发编程(五):并发模式
- 并发编程(六):原子操作与内存模型
- 数据库编程(一):SQL接口
- 数据库编程(二):ORM技术
- Web开发(一):路由与中间件
- Web开发(二):模板与静态资源
- Web开发(三):API开发
- Web开发(四):认证与授权
- Web开发(五):WebSocket
- 微服务(一):基础概念
- 微服务(二):gRPC入门
- 日志与监控
- 第三阶段项目实战:微服务聊天应用
📖 文章导读
在本文中,您将了解:
- Go语言sync包提供的各种同步原语及其使用场景
- 互斥锁(Mutex)和读写锁(RWMutex)的正确使用方法
- WaitGroup、Once和Cond等工具的实际应用
- Map和Pool等并发安全的数据结构
- 使用sync包的最佳实践和常见陷阱
虽然Go推崇"通过通信来共享内存"的CSP模型,但在某些场景下,使用传统的同步原语控制共享资源访问更加直接高效。本文将帮助你掌握sync包中的工具,以便在适当的场景下选择正确的并发控制机制。

并发编程(四):sync包
在前面的文章中,我们深入探讨了Go语言的并发基础:goroutine、channel和select语句。这些机制主要基于CSP(通信顺序进程)模型,强调"通过通信来共享内存"。然而,在某些情况下,我们需要直接控制对共享资源的访问。这时,Go语言标准库中的sync包就派上用场了。
本文将详细介绍sync包中提供的同步原语,帮助你在适当的场景下选择正确的同步工具。
一、Mutex与RWMutex
互斥锁(Mutex)和读写锁(RWMutex)是sync包中最常用的同步原语,用于保护共享资源免受并发访问的干扰。
1.1 Mutex(互斥锁)
Mutex提供了一种互斥机制,确保同一时间只有一个goroutine可以访问共享资源。
基本用法:
package main
import (
"fmt"
"sync"
"time"
)
func main() {
var mutex sync.Mutex
counter := 0
// 并发更新counter
var wg sync.WaitGroup
for i := 0; i < 1000; i++ {
wg.Add(1)
go func() {
defer wg.Done()
mutex.Lock() // 加锁
defer mutex.Unlock() // 解锁
counter++
}()
}
wg.Wait()
fmt.Println("计数器最终值:", counter) // 输出: 计数器最终值: 1000
}
重要方法:
Lock():获取锁。如果锁已被其他goroutine获取,则阻塞直到锁可用Unlock():释放锁。应在与Lock()相同的goroutine中调用TryLock():(Go 1.18+)尝试获取锁,如果锁不可用则立即返回false而不阻塞
最佳实践:
- 总是使用
defer mutex.Unlock()确保锁被释放 - 尽量减小临界区(加锁和解锁之间的代码)
- 避免在持有锁的情况下调用可能阻塞的操作
- 不要在goroutine A中锁定,然后在goroutine B中解锁
常见错误:
// 错误1: 忘记解锁
func increment(counter *int, mu *sync.Mutex) {
mu.Lock()
*counter++
// 忘记调用mu.Unlock(),导致死锁
}
// 错误2: 重复解锁
func decrement(counter *int, mu *sync.Mutex) {
mu.Lock()
*counter--
mu.Unlock()
mu.Unlock() // 错误: 再次解锁已经解锁的互斥量会导致panic
}
// 错误3: 复制互斥锁
func copyMutex() {
var mu sync.Mutex
mu.Lock()
muCopy := mu // 错误: 复制了互斥锁
muCopy.Unlock() // 未定义行为
}
1.2 RWMutex(读写锁)
RWMutex允许多个读操作并发执行,但写操作是互斥的。当有写锁时,所有的读操作都会被阻塞。
基本用法:
package main
import (
"fmt"
"sync"
"time"
)
func main() {
var rwMutex sync.RWMutex
data := make(map[string]string)
// 写入操作
go func() {
for i := 0; i < 10; i++ {
rwMutex.Lock() // 写锁
key := fmt.Sprintf("key%d", i)
data[key] = fmt.Sprintf("value%d", i)
time.Sleep(100 * time.Millisecond) // 模拟写入耗时
rwMutex.Unlock()
time.Sleep(200 * time.Millisecond) // 给读操作时间
}
}()
// 多个并发读取操作
var wg sync.WaitGroup
for r := 0; r < 5; r++ {
wg.Add(1)
go func(id int) {
defer wg.Done()
for i := 0; i < 10; i++ {
rwMutex.RLock() // 读锁
for k, v := range data {
fmt.Printf("读取者 %d: %s = %s\n", id, k, v)
}
rwMutex.RUnlock()
time.Sleep(150 * time.Millisecond)
}
}(r)
}
wg.Wait()
}
重要方法:
Lock()/Unlock():获取/释放写锁。与Mutex相同,写操作是互斥的RLock()/RUnlock():获取/释放读锁。多个goroutine可以同时持有读锁TryLock()/TryRLock():(Go 1.18+)尝试获取写锁/读锁,非阻塞
使用场景:
当共享资源的读操作远多于写操作时,RWMutex比Mutex更有效。例如:
- 配置信息:频繁读取,偶尔更新
- 缓存系统:大量读取,少量写入
- 统计数据:持续读取,定期更新
性能考虑:
- 如果读写比例接近1:1,普通的Mutex可能更高效
- RWMutex有额外的开销来跟踪读锁
- 写锁会阻塞所有的新读锁请求,可能导致"写饥饿"
二、WaitGroup
WaitGroup用于等待一组goroutine完成执行。它提供了一种简单的方式来协调多个并发操作的完成。
2.1 基本用法
WaitGroup的使用非常直观:
package main
import (
"fmt"
"sync"
"time"
)
func worker(id int, wg *sync.WaitGroup) {
defer wg.Done() // 工作完成时通知WaitGroup
fmt.Printf("Worker %d starting\n", id)
time.Sleep(time.Second) // 模拟工作
fmt.Printf("Worker %d done\n", id)
}
func main()

最低0.47元/天 解锁文章
14

被折叠的 条评论
为什么被折叠?



