Go 是一种有自动垃圾回收机制的编程语言, 采用三色并发标记算法标记对象并且回收.
但是, 如果想使用 Go 语言开发一个高性能的应用程序, 就必须考虑垃圾回收给性能带来影响的, 毕竟 Go 的自动垃圾回收机制有一个 STW (stop the world, 程序暂停)的时间, 而且在堆中大量地创建对象, 也会影响垃圾回收标记的时间.
在 Go 语言的垃圾回收(GC)机制中,有一个阶段叫做 STW(即 Stop-The-World)。 在这个阶段中,Go 语言的运行环境会暂停所有的 Goroutine,确保在进行垃圾回收的时候不会有新的垃圾产生。这就像是你在清扫房间的时候让所有的人都离开房间,这样能够避免有人在清扫的过程中再弄脏房间
因此, 需要在性能优化时, 通常会采用对象池的方式, 把不用的对象回收起来, 避免被垃圾回收, 这样使用时就不必在堆上重新创建对象. 还有一些长连接等, 如数据库连接, TCP的长连接, 这些连接的创建也是一个非常耗时的操作. 如果每次使用都创建一个新的连接, 则很可能整个业务的很多时间都花在创建连接上了. 所以, 如果把这些连接保存下来, 避免每次使用都重新创建, 则不仅可以大大减少业务的耗时, 还能提高应用程序的整体性能.
这种模式被称为对象池设计模式( object pool pattern ). 一个对象池包含一组已经初始化且可以重复使用的对象, 池的用户可以从池子里取得对象, 对其进行操作处理, 并且在不需要的时候返回给池, 而非直接销毁它, 等待下次再使用.
Go 标准库中提供一个通用的 Pool 数据结构, 也就是 sync.Pool, 我们使用它可以创建池化的对象. 但是 Pool 也有一些使用起来不太方便的地方, 比如它池化的对象可能会被垃圾回收, 这对于数据库长连接等场景是不合适.
sync.Pool 数据类型用来保存一组可访问的”临时”对象. 注意”临时”的意思是指的是其池化的对象会在未来某个时间被毫无预兆地移除. 而且, 如果没有别的对象引用这个要被移除的对象, 该对象就会被垃圾回收.
只提供三个对外的方法: New, Get 和 Put
New 创建对象 Pool 对象创建其池化对象的方法. 因为只有定义创建池化对象的方法, 它才能在需要的时候创建对象. Pool struct 包含一个 New 字段, 这个字段的类型是函数 func() interface{} 当调用 Pool 的 Get 方法从池中获取对象时, 如果没有更多空闲的对象可用, 就会调用 New 方法创建新的对象. 如果没有设置 New 字段, 当没有更多空闲的对象可返回时, Get 方法将返回 nil, 表明当前没有可用的对象
Pool 不需要初始化, 可以使用它的零值
Get 获取对象 如果调用 Get 方法, 就会从池子中取走一个对象. 这就意味着这个对象会被从池子移除, 返回给调用者. 不过, 除了返回值是正常实例化的对象, Get 方法可能返回 nil ( Pool.New 字段没有设置且没有空闲的对象可返回), 所以在使用的时候可能需要要判断.
Put 返还对象
Put 方法用于将一个对象返还给 Pool, Pool 会把这个对象保存到池子中, 并且可以复用. 但如果返还的是 nil, Pool 就会忽略这个值.
如果想弃用一个对象, 不再重用它, 则不再调用 Put 方法即可
package ch8
import (
"fmt"
"net/http"
"sync"
"time"
)
func Run() {
var p sync.Pool
p.New = func() interface{} {
return &http.Client{
Timeout: 5 * time.Second,
}
}
var wg sync.WaitGroup
wg.Add(10)
go func() {
for i := 0; i < 10; i++ {
go func() {
defer wg.Done()
c := p.Get().(*http.Client)
defer p.Put(c)
resp, err := c.Get("<https://bing.com>")
if err != nil {
fmt.Println("failed to get bing.com:", err)
return
}
resp.Body.Close()
fmt.Println("got bing.com")
}()
}
}()
wg.Wait()
}
在这个例子中, 为 New 定义了创建 http.Client 的方法, 然后启动 10 个 goroutine. 使用 http.Client 来访问一个网址. 在访问网址时, 首先从池子中获取一个 http.Client 对象. 使用完毕后再放回池子. 实际上, 这个 Pool 可能创建了 10个, 也可能了创建8个, 还可能创建了3个 http.Client , 就看用户从它那里请求时是否有空闲的 http.Client , 以及其他 goroutine 能不能及时把 http.Client 放回去.
这里没有检查从池子中获取的 http.Client 是否为空, 原因是我们为 New 字段复制了创建 http.Client 方法, 并且确保它能够返回一个 http.Client. 如果没有为 New 字段定义方法, 那么就需要检查 Get 方法的结果是否为 nil
可能会有一种不去设置 New 字段场景, 要求最多使用5个http.Client, 超过5个是不允许的, 那么就需要预先初始化5个http.Client, 不设置 New 字段, 就能保证不会超过5个http.Client
func Run1() {
var p sync.Pool
for i := 0; i < 5; i++ {
p.Put(&http.Client{
Timeout: 5 * time.Second,
})
}
var wg sync.WaitGroup
wg.Add(10)
go func() {
for i := 0; i < 10; i++ {
go func() {
defer wg.Done()
c, ok := p.Get().(*http.Client)
if !ok {
fmt.Println("got clilent is nil")
return
}
defer p.Put(c)
resp, err := c.Get("<https://bing.com>")
if err != nil {
fmt.Println("failed to get bing.com:", err)
return
}
resp.Body.Close()
fmt.Println("got bing.com")
}()
}
}()
}
这个例子没有设置 p.New 字段, 只是一开始就初始化了5个 http.Client. 运行这段代码, 大概率没有什么问题, 可能10次HTTP请求都能成功, 但是不设置 New 字段风险大, 因为池化的对象如果长时间没被使用, 可能就会被回收. 这和垃圾回收时机有关, 所以无法预测什么时候池化的对象会被回收.