前言
sync.Pool
是临时对象池,存储的是临时对象,不可以用它来存储 socket
长连接和数据库连接池等。
sync.Pool
本质是用来减少临时对象的重复创建,以减少内存分配,降低 GC 压力,提升程序的性能。如需要使用一个对象,就去 Pool 里面拿,如果拿不到就分配一个,这比起不停生成新的对象,用完了再等待 GC 回收要高效的多。
总之一句话:保存和复用临时对象,减少内存分配,降低 GC 压力。
1. sync.Pool 基本用法
sync.Pool
的使用很简单:
示例 1: 存储字符串
package main
import (
"fmt"
"sync"
)
func main() {
pool := &sync.Pool {
New: func() interface{} {
return "Hello!"
},
}
// 从池中取出对象
obj := pool.Get()
fmt.Println(obj) // 输出:Hello!
// 将对象放回池中
pool.Put(obj)
// 再次从池中取出对象
obj = pool.Get()
fmt.Println(obj) // 输出:Hello!
}
在上面的示例中,我们创建了一个Pool,池中存储的是字符串 “Hello!”。首先从池中取出对象,然后将其放回池中,最后再次取出对象。由于我们只创建了一个对象,所以两次输出的结果都是相同的。
示例 2:存储结构体对象
package util
import (
"sync"
)
type student struct {
Name string
Age int
}
var studentPool = &sync.Pool{
New: func() interface{} {
return new(student)
},
}
func New(jsonStr string) *student {
// 从 pool 取出对象,并强转为 (*student)
stu := studentPool.Get().(*student)
// Json字符串 转 struct
err := json.Unmarshal([]byte(jsonStr), stu)
if err != nil {
return nil
}
return stu
}
func Release(stu *student) {
stu.Name = ""
stu.Age = 0
studentPool.Put(stu)
}
package main
func main() {
str := `{"Name":"tom","Age":12}`
stu := util.New(str) // 调用 util 包名的函数
defer util.Release(stu)
// 业务逻辑
...
}
上面示例中声明了一个对象池,如果对象池不存在,将会使用 New 函数创建。通过 Get 方法从对象池获取对象,再将反序列化的内容赋值到对象中,一旦对象使用完毕,通过 Put 方法将对象返回对象池中。
示例3:使用 Get、Put
// 定义一个 Person 结构体,有Name和Age变量
type Person struct {
Name string
Age int
}
// 初始化sync.Pool,new函数就是创建Person结构体
func initPool() *sync.Pool {
return &sync.Pool{
New: func() interface{} {
fmt.Println("创建一个 person.")
return &Person{}
},
}
}
// 主函数,入口函数
func main() {
pool := initPool()
person := pool.Get().(*Person)
fmt.Println("首次从sync.Pool中获取person:", person)
person.Name = "Jack"
person.Age = 23
pool.Put(person)
fmt.Println("设置的对象Name: ", person.Name)
//time.Sleep(1 * time.Second) // 休息 1s 后, 取出的值就是新值了
fmt.Println("Pool 中有一个对象,调用Get方法获取:", pool.Get().(*Person))
fmt.Println("Pool 中没有对象了,再次调用Get方法:", pool.Get().(*Person))
}
打印:
创建一个 person.
首次从sync.Pool中获取person:&{ 0}
设置的对象Name: Jack
Pool 中有一个对象,调用Get方法获取:&{Jack 23}
创建一个 person.
Pool 中没有对象了,再次调用Get方法: &{ 0}
上面示例中,如果插入一条 time.Sleep(1 * time.Second)
(见注释部分),后面Get()
的Person对象都是新 New的。可见:
Get方法
并不会对获取到的对象值做任何的保证,因为放入本地对象池中的值有可能会在任何时候被删除。
使用 sync.Pool 的知识点:
- sync.Pool 是线程安全的,多个 goroutine 可以并发地调用存取对象;
New函数
:在创建 sync.Pool 时,需要传入一个New函数,当Get方法获取不到对象时,此时将会调用 New函数 创建新的对象并返回。Get方法
:从 sync.Pool 中取出缓存对象。Put方法
:将缓存对象放入到 sync.Pool 当中。
使用 sync.Pool 的步骤:
- 首先,使用 sync.Pool 定义一个对象缓冲池
- 在需要使用对象时,从缓冲池中取出
- 当使用完对象后,重新将对象放回缓冲池中
sync.Pool 的生命周期:
sync.Pool
中存储的对象并不会一直存在,它们的生命周期是由垃圾回收器控制的。
如果一个对象在一定时间内没有被使用,那么它就会被垃圾回收。这个时间是不确定的,具体什么时候真正释放,取决于垃圾回收器。
pool := &sync.Pool{
New: func() interface{} {
fmt.Println("New...")
return &Foo{Name: "foo"}
},
}
// 通过 Get 方法从对象池获取对象,并强转为 *Foo 指针类型
f := pool.Get().(*Foo)
f.Setname("emp")
fmt.Println(f.Name)
pool.Put(f)
time.Sleep(1 * time.Second) // 休息 1s 后, 取出的值又是新值
obj := pool.Get().(*Foo)
fmt.Println(obj.Name)
打印显示:
New...
emp
New...
foo
2. sync.Pool 高级用法
2.1 sync.Pool 的 GC 问题
由于 sync.Pool
中的对象是由垃圾回收器控制的,因此在使用 sync.Pool
时,需要注意避免对象被过早地回收。如果我们在使用对象时没有及时将其放回池中,那么垃圾回收器可能会将对象回收,从而导致程序出现问题。
为了避免这种情况发生,我们可以使用
sync.Pool
的Finalizer
方法。
- Finalizer 方法可以在对象被回收之前执行一些清理操作,从而保证对象在被回收之前能够被正确地处理。
2.2 sync.Pool 的并发安全性
sync.Pool
对象本身是并发安全的。这是因为内部使用了 sync.Mutex
来保证并发安全性。
注意:由于
sync.Pool
中存储的对象(结构体)是共享的,因此在使用对象时需做一些额外的同步操作,以避免出现并发问题。
例如,我们从池中取出一个对象,然后对其属性进行修改,那么其他 goroutine 可能也会同时访问到这个对象,从而导致并发问题。
type Counter struct {
mu sync.Mutex
count int
}
func (c *Counter) Add(n int) {
c.mu.Lock()
defer c.mu.Unlock()
c.count += n
}
func (c *Counter) Value() int {
c.mu.Lock()
defer c.mu.Unlock()
return c.count
}
func main() {
pool := &sync.Pool {
New: func() interface{} {
return &Counter{}
},
}
counter := pool.Get().(*Counter) // 从池中取出对象
counter.Add(1) // 对对象进行修改
pool.Put(counter) // 将对象放回池中
// 短时间内 再次从池中取出的对象也就是同一个对象
counter = pool.Get().(*Counter)
counter.Add(1) // 对对象进行修改
fmt.Println(counter.Value()) // 输出:2
}
我们对 Counter 对象做了额外的同步锁操作,从而保证其属性并发安全性。
3. 注意事项
-
需要注意放入对象的大小
如果不注意放入sync.Pool缓冲池中对象的大小,可能出现sync.Pool中只存在几个对象,却占据了大量的内存,导致内存泄漏 -
不要往 sync.Pool 中放入数据库连接 / TCP连接
对象池比较适合用来存储一些临时切状态无关的数据,但是不适合用来存储数据库连接的实例,因为存入对象池的值有可能会在垃圾回收时被删掉,这违反了数据库连接池建立的初衷。同时可能导致连接泄漏、连接池资源耗尽等问题。
连接对象泄漏的情况:
var pool = &sync.Pool{
New: func() interface{} {
conn, err := net.Dial("tcp", "localhost:8000")
if err != nil {
panic(err)
}
return conn
},
}
func main() {
// 模拟使用连接
for i := 0; i < 100; i++ {
conn := pool.Get().(net.Conn)
time.Sleep(100 * time.Millisecond)
fmt.Fprintf(conn, "GET / HTTP/1.0\r\n\r\n")
// 不关闭连接
...
// 不在使用连接时,释放连接对象到池中即可
pool.Put(conn)
}
}
在模拟使用连接时,我们从池中获取连接对象,向服务器发送一个 HTTP 请求,然后将连接对象释放到池中。但是,我们没有显式地关闭连接对象。如果连接对象的数量很多,那么这些未关闭的连接就会占用大量的内存资源,从而导致内存泄漏等问题。
小结
sync.Pool
对象池严格意义上来说是一个临时的对象池,适用于储存一些会在goroutine间分享的临时对象。主要作用是减少GC,提高性能。在Golang中最常见的使用场景就是fmt包中的输出缓冲区。