跳转到主内容
websoft网络软件专家 - 深耕网络技术,打造实用软件!

如何使用Golang的sync包同步goroutine_Golang并发同步与互斥操作方法

sync.Mutex 适合保护共享变量,需在访问前 Lock、结束后 Unlock;避免返回带锁字段指针;sync.Once 保证初始化仅一次且不可重置;sync.WaitGroup 要先 Add 再 go,用 defer Done;sync.RWMutex 适用于读多写少场景。 sync.Mutex 适合保护共享变量,但别在返回值里传指针 多个 goroutine 同时读写一个
int
、
map
或结构体字段时,不加锁大概率触发
fatal error: concurrent map writes
或读到脏数据。用
sync.Mutex
最直接:在访问前调用
mu.Lock()
,结束后立刻
mu.Unlock()
。 常见错误是把带锁的结构体字段以指针形式返回,比如:
func (c *Counter) GetPtr() *int { c.mu.Lock() return &c.val // 危险!外部可绕过锁直接改 }
应该改为只返回值,或提供受控的修改方法: 返回副本:
return c.val
封装操作:
func (c *Counter) Inc() { c.mu.Lock(); defer c.mu.Unlock(); c.val++ }
避免暴露内部状态,尤其别让调用方决定何时解锁 sync.Once 保证初始化只执行一次,但不能重置
sync.Once
的
Do()
方法适合做单例初始化、配置加载、资源首次创建等场景。它内部用原子操作 + 互斥锁实现,线程安全且开销极小。 立即学习 “ go语言免费学习笔记(深入) ”; 注意它不可重置、不可重复使用。一旦
once.Do(f)
执行过,再调用同个
once
实例的
Do()
将直接返回,不管
f
是否 panic(panic 会导致该次调用失败,但标记仍设为已执行)。 灵活路由的PHP库 灵活路由的PHP库 下载 不要试图通过新建
sync.Once
变量来“重试”初始化 如果初始化逻辑可能失败且需重试,应自己封装重试逻辑,而不是依赖
sync.Once
典型误用:
if err := initDB(); err != nil { once.Do(initDB) }
—— 这不会重试,因为
once
已标记完成 sync.WaitGroup 用于等待 goroutine 结束,别漏掉 Add 或 Done
sync.WaitGroup
是等待一组 goroutine 完成的最常用方式。核心是三步:
wg.Add(n)
、启动 goroutine、每个 goroutine 结尾调用
wg.Done()
,主 goroutine 调用
wg.Wait()
阻塞等待。 最容易出错的是调用时机:
Add()
必须在
go
语句之前,否则可能
Wait()
已返回而 goroutine 还没被计数;
Done()
必须确保执行,哪怕函数 panic,建议用
defer wg.Done()
。 错误写法:
go func() { wg.Add(1); ...; wg.Done() }()
——
Add
在 goroutine 内,竞态风险高 正确写法:
wg.Add(1); go func() { defer wg.Done(); ... }()
如果 goroutine 数量动态,先算好数量再
Add
,别靠
len(ch)
之类估算 sync.RWMutex 读多写少时提升性能,但写锁会阻塞所有读 当共享数据读操作远多于写操作(比如配置缓存、 路由 表),
sync.RWMutex
比普通
Mutex
更高效:多个 goroutine 可同时持有读锁,但写锁会独占,且会阻塞新读锁请求(已有读锁不强制释放)。 关键点在于锁粒度和使用习惯:读锁用
RLock()/RUnlock()
,写锁仍用
Lock()/Unlock()
。混用时注意——写锁必须等所有当前读锁释放后才能获取。 别在持有读锁时调用可能升级为写锁的函数(Go 不支持锁升级) 写操作频繁时,
RWMutex
可能比
Mutex
更慢,因读锁维护有额外开销 用
go tool trace
观察
sync
相关阻塞事件,确认是否真因读锁堆积导致延迟 实际并发控制中,最难的不是选哪个类型,而是判断“哪里需要同步”和“锁的边界是否覆盖全部临界区”。很多 bug 出现在看似无害的辅助函数里——比如一个日志函数悄悄修改了全局计数器,却没加锁。

相关文章