找回密码
 立即注册
首页 业界区 安全 go sync.pool 学习笔记

go sync.pool 学习笔记

郜庄静 4 小时前
概述

sync.pool  对象池可以用来复用临时对象,减少内存压力,降低 GC 压力。
示例

基本用法
  1. type Worker struct{}  
  2.   
  3. func (w *Worker) Name() string {  
  4.     return "worker"  
  5. }  
  6.   
  7. func main() {  
  8.     workerPool := sync.Pool{New: func() interface{} {  
  9.        return Worker{}  
  10.     }}  
  11.   
  12.     worker := workerPool.Get().(Worker)  
  13.     defer workerPool.Put(worker)  
  14.   
  15.     name := worker.Name()  
  16.     fmt.Println(name)  
  17. }
复制代码
sync.pool 是单对象池,不是多对象池。基本使用方法是 Get 和 Put 方法,Get 用来从对象池中取对象,Put 用来将不用的对象放回对象池中。
适用场景

sync.pool 中的对象可能会被运行时回收。有可能在需要使用时对象被回收而重新创建。因此,sync.pool 适合存储高频创建,作用时间短的对象。比如以下场景:

  • JSON 处理:频繁分配的 []byte 切片;
  • Web 服务:HTTP 请求处理的缓冲区;
  • 数据库操作:连接池的辅助工具;
Go sync.Pool 的陷阱与正确用法:从踩坑到最佳实践 这篇文章写的很好关于 sync.pool 的陷阱和正确用法,可以参考学习,这里就不赘述了。
性能测试
  1. var globalBuf []byte  
  2.   
  3. func BenchmarkAllocateWithoutPool(b *testing.B) {  
  4.     for i := 0; i < b.N; i++ {  
  5.        buf := make([]byte, 1024)  
  6.        globalBuf = buf     // 这里将 buf 赋值给 globalBuf,不然会内存逃逸
  7.     }  
  8. }  
  9.   
  10. func BenchmarkAllocateWithPool(b *testing.B) {  
  11.     pool := sync.Pool{New: func() interface{} { return make([]byte, 1024) }}  
  12.     b.ResetTimer()  
  13.     for i := 0; i < b.N; i++ {  
  14.        buf := pool.Get().([]byte)  
  15.        pool.Put(buf)  
  16.     }  
  17. }
复制代码
测试结果:
  1. go test -bench . -benchmem
  2. goos: darwin
  3. goarch: arm64
  4. pkg: go-by-example/sync/pool
  5. cpu: Apple M3
  6. BenchmarkAllocateWithoutPool-8           8672882               137.2 ns/op          1024 B/op          1 allocs/op
  7. BenchmarkAllocateWithPool-8             36728509                31.91 ns/op           24 B/op          1 allocs/op
  8. PASS
  9. ok      go-by-example/sync/pool 3.047s
复制代码
Go Benchmark的输出格式为:
  1. BenchmarkName-GOMAXPROCS Iterations TimePerOp(ns/op) BytesPerOp(B/op) AllocsPerOp(allocs/op)
复制代码

  • TimePerOp:单次操作耗时(纳秒),越小越快。
  • AllocsPerOp:单次操作的内存分配次数,越小对GC越友好。
  • BytesPerOp:单次操作分配的总字节数,越小内存效率越高。
可以看出,使用 sync.pool 对象池相比于不使用 sync.pool 的性能对比:

  • 单次操作耗时占比:31.91 / 137.2 = 23.2%
  • 单次操作分配内存:24/1024 = 2.3%
并发

我们进一步看并发场景下对象复用是什么情况。
非并发场景
首先看非并发场景对象复用情况。示例如下:
  1. type Worker struct{}  
  2.   
  3. func (w *Worker) Name() string {  
  4.     return "worker"  
  5. }
  6. func main() {  
  7.     runtime.GOMAXPROCS(4)
  8.    
  9.         var createWorkerTime int32  
  10.         workerPool := sync.Pool{New: func() interface{} {  
  11.             atomic.AddInt32(&createWorkerTime, 1)
  12.             return Worker{}  
  13.         }}
  14.        
  15.         currencyCount := 1024 * 1
  16.     for i := 0; i < currencyCount; i++ {  
  17.        worker := workerPool.Get().(Worker)  
  18.        time.Sleep(time.Millisecond * 1)  
  19.        workerPool.Put(worker)  
  20.     }  
  21.   
  22.     fmt.Println("create worker time: ", atomic.LoadInt32(&createWorkerTime))  
  23. }
复制代码
输出:
  1. create worker time:  1
复制代码
这里对象只创建了一次。
需要注意的是,sync.pool 的 Get 和 Put 是并发安全的。但是创建对象并不是并发安全的,需要用户自己实现。因此,在 sync.pool.New 中使用 atomic.AddInt32 原子操作并发安全的更新 createWorkerTime 变量。
并发场景
示例如下:
  1. func main() {  
  2.     runtime.GOMAXPROCS(4)  
  3.   
  4.     var createWorkerTime int32  
  5.     workerPool := sync.Pool{New: func() interface{} {  
  6.        atomic.AddInt32(&createWorkerTime, 1)  
  7.        return Worker{}  
  8.     }}  
  9.   
  10.     currencyCount := 1024 * 1  
  11.     var wg sync.WaitGroup  
  12.     for i := 0; i < currencyCount; i++ {  
  13.        wg.Add(1)  
  14.        go func(i int) {  
  15.           defer wg.Done()  
  16.           worker := workerPool.Get().(Worker)  
  17.           defer workerPool.Put(worker)
  18.             }(i)  
  19.         }  
  20.   
  21.         wg.Wait()  
  22.         fmt.Println("create worker time: ", atomic.LoadInt32(&createWorkerTime))
  23. }
复制代码
输出:
  1. ~/project/go-by-example/sync/pool git:[main] go run main.go
  2. create worker time:  4
  3. ~/project/go-by-example/sync/pool git:[main] go run main.go
  4. create worker time:  4
  5. ~/project/go-by-example/sync/pool git:[main] go run main.go
  6. create worker time:  4
复制代码
我们仅调整 runtime.GOMAXPROCS 为 8,运行程序输出:
  1. ~/project/go-by-example/sync/pool git:[main] go run main.go
  2. create worker time:  5
  3. ~/project/go-by-example/sync/pool git:[main] go run main.go
  4. create worker time:  6
  5. ~/project/go-by-example/sync/pool git:[main] go run main.go
  6. create worker time:  7
复制代码
当调整 runtime.GOMAXPROCS 时,对象创建次数不固定。要解释其中发什么了什么,需要从 GPM 入手,runtime.GOMAXPROCS 设置 P 的个数,P 会调度 G 到线程 M 上运行,而对象是 P 私有的,如果 G 上的 P 没有对象,则会创建对象。这也解释了,为什么 P 变多了会影响对象的复用次数。
继续构造示例如下:
  1. currencyCount := 1024 * 1  
  2. var wg sync.WaitGroup  
  3. for i := 0; i < currencyCount; i++ {  
  4.     wg.Add(1)  
  5.     go func(i int) {  
  6.        defer wg.Done()  
  7.        worker := workerPool.Get().(Worker)  
  8.        defer workerPool.Put(worker)  
  9.        time.Sleep(time.Millisecond * 100)  
  10.     }(i)  
  11. }
复制代码
我们在协程内加了 time.Sleep(time.Millisecond * 100) 运行三次程序:
  1. ~/project/go-by-example/sync/pool git:[main] go run main.go
  2. create worker time:  1024
  3. ~/project/go-by-example/sync/pool git:[main] go run main.go
  4. create worker time:  1024
  5. ~/project/go-by-example/sync/pool git:[main] go run main.go
  6. create worker time:  1024
复制代码
更新 runtime.GOMAXPROCS 在此运行三次程序,结果都是 1024。
这是为什么呢?
还是和 GPM 有关,对象是 P 私有的,P 调度 G 到协程 M 上运行,如果 P 有对象,则会将对象给 G,将私有的对象置为 nil,下次分配对象时如果没有对象,则会调用 sync.pool.New 创建对象。
这里 G 拿到 P 的私有对象后,在线程 M 上运行。由于设置了 time.Sleep G 陷入阻塞状态,M 会运行下一个 G,下一个 G 发现 P 的私有对象已经被阻塞的 G 拿掉了,又会调用 sync.pool.New 创建对象。如此重复,导致每次对象都在创建。
基于这样的逻辑,我们在构造示例如下:
  1. currencyCount := 1024 * 1  
  2. var wg sync.WaitGroup  
  3. for i := 0; i < currencyCount; i++ {  
  4.     wg.Add(1)  
  5.     go func(i int) {  
  6.        defer wg.Done()  
  7.        worker := workerPool.Get().(Worker)  
  8.        defer workerPool.Put(worker)  
  9.        name := worker.Name()  
  10.        fmt.Println("worker name: ", name, "currency id: ", i)  
  11.     }(i)  
  12. }
复制代码
输出:
  1. ~/project/go-by-example/sync/pool git:[main] go run main.go
  2. create worker time:  980
  3. ~/project/go-by-example/sync/pool git:[main] go run main.go
  4. create worker time:  930
  5. ~/project/go-by-example/sync/pool git:[main] go run main.go
  6. create worker time:  884
复制代码
这里没有用 time.Sleep 使 G 陷入阻塞,而是打印对象的名字。输出的对象创建次数并不固定。
这是因为在有些 P 上, 当前 G 执行完将对象 Put 归还给 P 了,下一个 G 会从 P 上拿到对象。而有些 P,当前 G 并未将对象归还给 P,而下一个 G 又找 P 要对象,触发创建对象逻辑,导致每次运行创建对象的次数都不一样。
小结

本文介绍了 sync.pool 的使用,性能分析及并发场景下的对象复用情况,对于 sync.pool 的原理级了解还是要从源码层面入手。
参考资料


  • Go sync.Pool 的陷阱与正确用法:从踩坑到最佳实践
  • Go sync.Pool

来源:程序园用户自行投稿发布,如果侵权,请联系站长删除
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!

相关推荐

您需要登录后才可以回帖 登录 | 立即注册