Go语言单写入者单次写+多读取场景的同步方法选型
单写(仅一次)多读场景下的Go同步优化方案
问题背景
本场景的特殊性在于:写入者唯一且写入操作严格仅执行一次,同时存在大量并发读取者(比如并发调用的业务方法)。当前用sync.RWMutex实现同步,虽然比普通sync.Mutex性能更优,但全程加锁保护仅一次的写入,存在不必要的资源浪费,有没有更高效的实现方式?
最优方案:使用sync.Once
Go标准库中的sync.Once正是为"某个操作仅执行一次"的场景设计的同步原语,它通过原子操作保证写入(初始化)仅执行一次,后续读取无需加锁,能大幅降低高并发场景下的开销。
优化后的代码实现
package main import ( "sync" ) type OnceErrorNotifier struct { once sync.Once emailSender func(string) } func (a *OnceErrorNotifier) SetEmailSenderService(emailSender func(string)) { a.once.Do(func() { a.emailSender = emailSender }) } func (a *OnceErrorNotifier) SendErrorMessage(errorMessage string) { // sync.Once保证写入完成后变量对所有goroutine可见,无需加锁读取 if a.emailSender == nil { return } a.emailSender(errorMessage) } func main() { sender := &OnceErrorNotifier{} errorsCount := 100_000 emailSender := func(emailMessage string) { // sending email... } var wg sync.WaitGroup // 仅用于演示并发场景 wg.Add(errorsCount) for i := 0; i < errorsCount; i++ { go func() { sender.SendErrorMessage("ALARM!") wg.Done() }() } sender.SetEmailSenderService(emailSender) // 仅执行一次的写入操作 wg.Wait() }
方案对比说明
- 原RWMutex方案:每次读取都需要获取读锁,即使写入已经完成,仍会有锁竞争带来的性能损耗。
- sync.Once方案:
- 写入操作通过
Do方法保证仅执行一次,内部通过原子操作+懒加载锁实现,仅第一次调用Do时会进入锁定逻辑。 - 写入完成后,所有读取操作直接访问变量,无需任何锁开销,在高并发读取场景下性能提升显著。
- 写入操作通过
注意事项
sync.Once的Do方法仅保证传入的函数执行一次,即使多次调用SetEmailSenderService,后续调用也不会修改emailSender的值——这完全匹配本场景"严格仅一次写入"的要求;如果需要支持多次写入,仍需使用RWMutex。- 变量可见性:
sync.Once已经内置内存屏障,保证写入完成后,变量对所有goroutine可见,无需额外处理。
内容的提问来源于stack exchange,提问作者IvanD
相关产品推荐
相关产品推荐

