NSQD对sync.Once的使用及sync.Once探索

本文详细介绍了Go语言中的并发原语sync.Once的使用,包括其在NSQD启动过程中的应用,以及如何确保资源只初始化一次或执行一次退出操作。sync.Once通过一个done标志和互斥锁实现,避免了并发安全问题。文中还探讨了错误的实现方式,如死锁和异常导致的资源初始化不完整,并提供了标准库源码的分析,帮助理解其内部工作原理。此外,文章通过实例展示了如何在实际代码中正确使用sync.Once来初始化单例资源。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

1、NSQD 启动过程中对sync.Once的使用

源码路径:apps/nsqd/main.go

核心结构体:

 

type program struct {
   once sync.Once
   nsqd *nsqd.NSQD
}

在使用Once的时候,可以尝试采用此种结构,将值和Once封装成一个新的数据机构,提供只初始化一次或只结束一次的值。

例如,nsqd中,在Stop()方法里面,利用sync.Once保证,对nsqd执行且仅仅执行一次退出操作

 

func (p *program) Stop() error {
   p.once.Do(func() {
      p.nsqd.Exit()
   })
   return nil
}

2、sync.Once探索

(1)什么是sync.Once

 

Once就是一个结构体,且此结构体只有两个属性done uint32 和 m Mutex.

Once可以用来执行且仅仅执行一次动作

并且通过源码注释可以发现,done 在此结构体的第一个位置是为了能够在 hot path 中被用到。

hot path 能够在函数每次调用时被内联调用。

关于Go语言的inline内联调用可以查看此文章:

https://blog.csdn.net/hero_java/article/details/114029057

 

(2)Once 的使用场景

sync.Once只暴露了一个方法Do,你可以多次调用Do 方法,但是只有第一次调用Do方法时f参数才会执行,这里的f参数是一个无参无返回值的函数。

 

// Do calls the function f if and only if Do is being called for the
// first time for this instance of Once. In other words, given
//     var once Once
// if once.Do(f) is called multiple times, only the first call will invoke f,
// even if f has a different value in each invocation. A new instance of
// Once is required for each function to execute.
func (o *Once) Do(f func()) 

当且仅当第一次调用Do 方法的时候参数f才会执行,即使第一次、第二次、第三次、第n次调用时f参数的值不一样,也不会被执行。例如:

 

package main

import (
   "fmt"
   "sync"
)

func main() {
   var once sync.Once

   f1 := func() {
      fmt.Printf("in func1")
   }
   once.Do(f1) // 打印出 in func1

   f2 := func() {
      fmt.Printf("in func2")
   }
   once.Do(f2) // 无输出

   f3 := func() {
      fmt.Printf("in func3")
   }
   once.Do(f3) //无输出
}

虽然f1、f2和f3是不同的函数,但是第二个函数f2 和 第三个函数f3不会执行。

通过sync.Once的源码注释

 

// Do is intended for initialization that must be run exactly once. Since f
// is niladic, it may be necessary to use a function literal to capture the
// arguments to a function to be invoked by Do:
//     config.once.Do(func() { config.init(filename) })
func (o *Once) Do(f func())

可以看出因为这里的f 是一个无参数无返回值的函数,所有可以通过 闭包 的方式引用外面的参数。比如:

 

var addr = "baidu.com"

var conn net.Conn
var err error
once.Do(func(){
    conn,err = net.Dial("tcp",addr)
})

在实际的使用过程中,绝大数情况下,你会使用闭包的方式去初始化外部的一个资源。

例如在math/big/sqrt.go中,实现的一个数据结构,它通过Once封装了一个只初始化一次的值。

 

//值是3.0或0.0的一个数据结构
var threeOnce struct {
   sync.Once
   v *Float
}

func three() *Float {
   threeOnce.Do(func() { //使用Once初始化
      threeOnce.v = NewFloat(3.0)
   })
   //返回此数据结构的值,如果还没有初始化为3.0,则进行初始化
   return threeOnce.v
}

因此可以总结出sync.Once这个并发原语要解决的问题和使用的场景为:用来初始化单例资源、并发访问时只需要初始化一次的共享资源或只需要执行一次的退出操作。

 

(3)Once 的实现

标准库中的实现为:

 

type Once struct {
  done uint32
   m    Mutex
}

func (o *Once) Do(f func()) {

   if atomic.LoadUint32(&o.done) == 0 {  // fast-path
      // Outlined slow-path to allow inlining of the fast-path.
      o.doSlow(f)
   }
}

func (o *Once) doSlow(f func()) {
   o.m.Lock()
   defer o.m.Unlock()
   //双重检查
   if o.done == 0 {
      defer atomic.StoreUint32(&o.done, 1) //不会内联
      f()
   }
}

所以,通过标准库的源码实现可以学习到,一个正确的 Once实现要使用一个互斥锁,这样初始化的时候,如果有并发的goroutine,就会执行doSlow方法。互斥锁的机制保证只有一个goroutine进行初始化。同时在doSlow方法中利用双检查机制(double-checking),再次判断o.done的值是否为0,如果为0,则是第一次执行,执行完成后,通过 defer 将o.done设置为1,最后释放锁。

如果此时,有更多的goroutine同时执行doSlow方法,因为双检查机制,后续的goroutine只会看到o.done的值为1,不会再次去执行f函数。

 

这种从XXX方法中,单独抽出XXXSlow方法去执行的好处是,分离固定的静态逻辑和动态逻辑,使得固定静态的逻辑能够被内联调用,提高执行效率。

 

在源码注释中,有一个错误的实现示例:

 

// Note: Here is an incorrect implementation of Do:
//
// if atomic.CompareAndSwapUint32(&o.done, 0, 1) {
//    f()
// }
//
// Do guarantees that when it returns, f has finished.
// This implementation would not implement that guarantee:
// given two simultaneous calls, the winner of the cas would
// call f, and the second would return immediately, without
// waiting for the first's call to f to complete.
// This is why the slow path falls back to a mutex, and why
// the atomic.StoreUint32 must be delayed until after f returns

这个实现会有一个很大的问题,如果有两个goroutine同时执行,先抢到的goroutine将会调用f,则第二个则会立即返回,不会去等第一个调用f完成。如果f逻辑很复杂,执行效率很慢的话,后续调用Do方法的goroutine虽然看到done已经设置为执行过了,但是获取某些初始化资源的时候,可能会得到空的资源,因为f还没有执行完。

所以,正确的实现方式就是利用互斥锁+双检查机制。

 

(4)Once 的错误使用方式

在源码注释中,有进行说明:

 

// Because no call to Do returns until the one call to f returns, if f causes
// Do to be called, it will deadlock.
//
// If f panics, Do considers it to have returned; future calls of Do return
// without calling f.

第一种情况:死锁

因为只有对f的调用返回时,对Do的调用才会返回,如果f中,有引起再次调用这个Once的Do方法的话,会导致死锁情况的出现。这是Lock的递归调用导致的死锁。例如:

 

func main() {
    var once sync.Once
    once.Do(func() {
        once.Do(func() {
            fmt.Println("Init...")
        })
    })
}

第二种情况:异常导致初始化资源不完整

如果 f 方法执行的时候 panic,或者 f 执行初始化资源的时候失败了,Once 还是会认为初次执行已经成功了,即使再次调用 Do 方法,也不会再次执行 f。

 

<think>好的,我现在需要回答用户的问题:“优秀的go并发项目参考”。用户之前已经询问过关于Go并发优秀源码的问题,现在他们可能希望了解一些具体的开源项目,这些项目在实际中如何应用Go的并发特性。我需要确保回答的结构清晰,分层次,并且提供有用的信息。 首先,回顾之前的回答,用户已经得到了一些标准库和开源项目的例子,比如Etcd和Docker。现在用户可能想要更多的项目参考,或者更详细的项目列表,以便进一步学习。我需要扩展这些例子,并添加更多不同类型的项目,展示Go并发在不同场景下的应用。 接下来,我需要考虑用户的潜在需求。他们可能是开发者,想通过研究实际项目来提升自己的并发编程能力。因此,项目应具有代表性,代码结构清晰,且广泛被认可。同时,项目应涵盖不同领域,如分布式系统、网络服务、数据库等,以展示Go并发的多样性。 我需要收集一些知名的Go项目,特别是那些以并发处理著称的。例如: 1. **Kubernetes**:作为容器编排系统,大量使用Go的并发模型处理集群管理。 2. **CockroachDB**:分布式SQL数据库,利用Go的并发处理分布式事务和一致性。 3. **Tyk**:API网关,处理高并发请求。 4. **InfluxDB**:时间序列数据库,高效处理并发写入和查询。 5. **NSQ**:实时分布式消息平台,使用Go的channel进行消息传递。 6. **Jaeger**:分布式追踪系统,处理并发数据收集和查询。 7. **Go-kit**:微服务工具包,提供并发模式的最佳实践。 8. **Gorilla WebSocket**:处理WebSocket连接的并发管理。 9. **Traefik**:反向代理和负载均衡器,处理高并发连接。 10. **Prometheus**:监控系统,并发抓取和处理指标数据。 每个项目需要简要介绍其并发应用场景和核心机制,例如使用goroutines、channels、sync包的原语等。同时,可以指出源码中的关键文件或模块,帮助用户快速定位学习重点。 还需要注意避免重复之前提到的项目,如Etcd和Docker,但可以补充更多细节。此外,提供学习建议,如如何有效阅读源码,使用工具调试并发问题,以及推荐的资源,帮助用户深入学习。 在结构上,可以按项目类型分类,如分布式系统、数据库、消息队列、网络服务等,每个类别下列举几个项目,并说明其并发实现的特点。这样用户可以根据兴趣或需求选择适合的项目进行研究。 需要确保信息准确,项目确实使用Go并发特性,并且是活跃的开源项目。另外,每个项目的描述要简明扼要,突出其并发方面的亮点,例如如何利用channels进行通信,使用goroutines处理任务,或者如何优化并发性能。 最后,总结部分可以强调通过研究这些项目,用户能学到哪些并发模式或最佳实践,如工作池、扇入扇出、并行处理管道等,并推荐进一步的学习资源或方法。</think>以下是一些优秀的Go并发项目参考,这些项目在并发模型设计和实现上具有示范意义,适合深入学习Go的高并发处理机制: --- ### 一、分布式系统与协调 1. **Kubernetes (k8s)** - **核心并发场景**:协调百万级容器的状态同步、事件处理 - **关键实现**: - `client-go` 中的 `Informer` 机制(`tools/cache/shared_informer.go`) 使用 `Reflector` + `Delta FIFO` 队列监听资源变更,通过 `goroutine` 异步处理事件 - 控制器模式:多个控制器并发运行,通过 `WorkQueue` 实现任务分发 - **源码重点**:`staging/src/k8s.io/client-go/tools/cache/` 2. **CockroachDB** (分布式SQL数据库) - **并发亮点**: - 使用`Raft`共识算法实现多副本一致性(`pkg/storage/raft_transport.go`) - 分布式事务的并发控制(`pkg/sql/execinfra/` 中的并行查询执行) - **关键技术**: - 无锁数据结构优化读写冲突 - `context` 包实现操作级联取消 --- ### 二、高性能网络服务 3. **NSQ** (实时分布式消息平台) - **并发模型**: - 每个消息通道(`Channel`)独立运行处理协程(`nsqd/channel.go`) - 使用 `sync.Pool` 重用内存降低GC压力 - **核心文件**: - `nsqd/protocol_v2.go`:处理TCP连接的读写协程 - `internal/protocol/http.go`:HTTP API的并发请求处理 4. **Traefik** (云原生反向代理) - **并发机制**: - 中间件链的并行处理(`pkg/middlewares/chain.go`) - 动态配置的热加载(`pkg/provider/configuration.go` 中的原子操作替换配置) - **设计模式**: - 使用 `channel` 传递请求上下文 - `sync.Once` 确保单次初始化 --- ### 三、存储与数据处理 5. **InfluxDB** (时间序列数据库) - **并发优化**: - TSM引擎的并行数据压缩(`tsdb/engine/tsm1/compact.go`) - 使用 `mmap` 实现零拷贝并发读取 - **关键技术**: - `RWMutex` 控制分片(Shard)的读写访问 - `go-dedup` 算法优化重复数据删除 6. **Dgraph** (分布式图数据库) - **亮点实现**: - 基于 `Badger` KV存储的并发事务处理(`dgraph/x/`) - 使用 `gRPC` 流式接口实现多节点并行查询 - **源码重点**: - `worker/palimir.go`:分布式查询的并发执行计划 --- ### 四、工具与框架 7. **Go-kit** (微服务工具包) - **并发实践**: - 中间件的并发安全设计(`transport/http/server.go`) - 断路器模式(`circuitbreaker` 包中的状态机原子操作) - **学习重点**: - `endpoint` 链式调用的并发控制 8. **Gorilla WebSocket** - **并发处理**: - 每个WebSocket连接独立读写协程(`conn.go` 中的 `readLoop`/`writeLoop`) - 使用 `sync.Mutex` 保护共享缓冲区 - **抗并发问题**: - 通过 `CloseHandler` 处理并发关闭连接 --- ### 五、学习建议 1. **源码阅读技巧**: - 使用 `go doc` 查看接口定义(如 `go doc sync.Map`) - 结合 `pprof` 分析goroutine运行状态 ```bash go tool pprof -http=:8080 http://localhost:6060/debug/pprof/goroutine ``` 2. **调试工具**: - **Goroutine泄漏检测**:使用 `go.uber.org/goleak` - **竞态检测**:`go build -race` - **调度跟踪**:`GODEBUG=scheddetail=1,schedtrace=1000` 3. **推荐学习路径**: ``` 基础并发原语 → Channel高级模式 → 并发安全设计 → 分布式系统应用 ``` 这些项目展示了Go并发在实践中的典型应用模式: - **通信顺序进程(CSP)**:通过Channel解耦组件 - **工作窃取**:优化任务分配效率 - **无锁编程**:原子操作替代传统锁 - **并发控制**:Context传播与超时管理 通过研究这些项目的源码,可以深入理解如何在高并发场景下平衡性能与正确性。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

DreamCatcher

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值