sync.Pool
是 sync
包下的一个组件,可以作为保存临时取还对象的一个“池子”,可以缓存暂时不用的对象,下次需要时直接使用(无需重新分配)。因为频繁的内存分配和回收会对性能产生影响,通过复用临时对象可以避免该问题。
适用场景: 当多个 goroutine 都需要创建同一个对象时,如果 goroutine 数过多,导致对象的创建数目剧增,进而导致 GC 压力增大。形成 “并发大-占用内存大-GC 缓慢-处理并发能力降低-并发更大”这样的恶性循环。在这个时候,需要有一个对象池,每个 goroutine 不再自己单独创建对象,而是从对象池中获取出一个对象(如果池中已经有的话)。
关键思想:对象的复用,避免重复创建和销毁,减少频繁的内存分配和回收,从而减少 GC 压力。
sync.Pool
是协程安全的,使用前,设置好对象的 New
函数,用于在 Pool 里没有缓存的对象时,创建一个。之后,在程序的任何地方、任何时候仅通过 Get()
、 Put()
方法就可以取、还对象了。
以下为基本使用示例:
package main
import (
"fmt"
"sync"
)
type Gopher struct {
Name string
Remark [1024]byte
}
func (s *Gopher) Reset() {
s.Name = ""
s.Remark = [1024]byte{}
}
var gopherPool = sync.Pool{
New: func() interface{} {
return new(Gopher)
},
}
func main() {
g := gopherPool.Get().(*Gopher)
fmt.Println("首次从 pool 里获取:", g.Name)
g.Name = "first"
fmt.Printf("设置 p.Name = %s\n", g.Name)
gopherPool.Put(g)
fmt.Println("Pool 里已有一个对象:", gopherPool.Get().(*Gopher).Name)
fmt.Println("Pool 没有对象了,调用 Get: ", gopherPool.Get().(*Gopher).Name)
}
运行结果:
首次从 pool 里获取:
设置 p.Name = first
Pool 里已有一个对象: first
Pool 没有对象了,调用 Get:
初始化 Pool
时,唯一需要的是设置好 New
函数。当调用 Get
方法时,如果池子里缓存了对象,就直接返回缓存的对象。如果没有存货,则调用 New
函数创建一个新的对象。注意,我们不应对 Get
方法取出来的对象有任何假设,最好的做法是在 Put
前,将对象清空。
sync.Pool
的结构体定义如下:
type Pool struct {
noCopy noCopy
local unsafe.Pointer // local fixed-size per-P pool, actual type is [P]poolLocal
localSize uintptr // size of the local array
victim unsafe.Pointer // local from previous cycle
victimSize uintptr // size of victims array
New func() interface{} // New is called when a new instance is needed
}
Get
方法的主要逻辑包括从当前 Goroutine 绑定的 P 的本地池中获取对象,如果本地池为空,则尝试从全局池中获取对象,如果还没有,则调用用户提供的 New
方法创建对象。
Put
方法将对象放回到当前 Goroutine 绑定的 P 的本地池中,如果本地池已经满了,则放入到全局池中。
为了防止内存泄漏和无限制增长,sync.Pool
在 GC 时会清理池中的对象。在 pool.go
文件的 init
函数里,注册了 GC 发生时,如何清理 Pool 的函数:
func init() {
runtime_registerPoolCleanup(poolCleanup)
}
每个 P 对应一个本地池(local
字段),本地池的类型是 poolLocal
,结构如下:
type poolLocal struct {
private interface{} // Can be used only by the respective P.
shared poolChain // Can be used by any P.
}
其中,private
字段仅能被对应的 P 使用,shared
字段可以被任何 P 使用。shared
字段类型是 poolChain
,它实际上是一个双向链表,用于存储多个对象。
如果本地池中没有对象时,会尝试从全局池中获取对象,全局池是一个 poolDequeue
类型的双向链表。
为了提高对象获取的成功率,sync.Pool
引入了 victim
机制。在 GC 时,会将本地池的对象移动到 victim
池中,victim
池中的对象可以在下一个 GC 周期前被使用。
在高并发的 HTTP 服务器中,可以使用 sync.Pool
缓存请求对象,以减少频繁的内存分配和回收。
package main
import (
"net/http"
"sync"
)
var requestPool = sync.Pool{
New: func() interface{} {
return new(http.Request)
},
}
func handler(w http.ResponseWriter, r *http.Request) {
req := requestPool.Get().(*http.Request)
*req = *r
// 处理请求
requestPool.Put(req)
}
func main() {
http.HandleFunc("/", handler)
http.ListenAndServe(":8080", nil)
}
在 JSON 编码和解码时,可以使用 sync.Pool
缓存 json.Encoder
和 json.Decoder
对象,以提高性能。
package main
import (
"encoding/json"
"sync"
)
var encoderPool = sync.Pool{
New: func() interface{} {
return json.NewEncoder(nil)
},
}
var decoderPool = sync.Pool{
New: func() interface{} {
return json.NewDecoder(nil)
},
}
func main() {
// 使用 encoderPool 和 decoderPool 进行 JSON 编码解码
}
sync.Pool
是协程安全的,使用起来非常方便。设置好 New
函数后,调用 Get
获取,调用 Put
归还对象。一些设计思想或者相关知识点:无锁编程、原子操作代替锁、cacheline false sharing 问题、noCopy 禁止复制、分段锁、victim cache 等。
如果您喜欢我的文章,请点击下面按钮随意打赏,您的支持是我最大的动力。
最新评论