使用 sync.Pool 提高程序性能

文章介绍了sync.Pool在Go语言中的使用,它是用于减少临时对象创建、内存分配和降低GC压力的工具。通过示例展示了如何使用sync.Pool存储字符串、结构体对象以及注意事项,包括并发安全和避免存储数据库连接等。
摘要由CSDN通过智能技术生成

前言

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.PoolFinalizer 方法。

  • 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包中的输出缓冲区。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值