Go语言之 sync.Pool

sync.Poolsync 包下的一个组件,可以作为保存临时取还对象的一个“池子”,可以缓存暂时不用的对象,下次需要时直接使用(无需重新分配)。因为频繁的内存分配和回收会对性能产生影响,通过复用临时对象可以避免该问题。

适用场景: 当多个 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 前,将对象清空。

2. 源码分析

Pool 结构体

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 方法

Get 方法的主要逻辑包括从当前 Goroutine 绑定的 P 的本地池中获取对象,如果本地池为空,则尝试从全局池中获取对象,如果还没有,则调用用户提供的 New 方法创建对象。

Put 方法

Put 方法将对象放回到当前 Goroutine 绑定的 P 的本地池中,如果本地池已经满了,则放入到全局池中。

GC 清理

为了防止内存泄漏和无限制增长,sync.Pool 在 GC 时会清理池中的对象。在 pool.go 文件的 init 函数里,注册了 GC 发生时,如何清理 Pool 的函数:

func init() {
    runtime_registerPoolCleanup(poolCleanup)
}

3. 详细源码分析

本地池

每个 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 类型的双向链表。

victim 机制

为了提高对象获取的成功率,sync.Pool 引入了 victim 机制。在 GC 时,会将本地池的对象移动到 victim 池中,victim 池中的对象可以在下一个 GC 周期前被使用。

4. 使用案例

案例一:HTTP 服务器

在高并发的 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 编码解码

在 JSON 编码和解码时,可以使用 sync.Pool 缓存 json.Encoderjson.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 编码解码
}

5. 总结

  1. 关键思想是对象的复用,避免重复创建、销毁,将暂时不用的对象缓存起来,待下次需要的时候直接使用,不用再次经过内存分配,复用对象的内存,减轻 GC 的压力。
  2. sync.Pool 是协程安全的,使用起来非常方便。设置好 New 函数后,调用 Get 获取,调用 Put 归还对象。
  3. 不要对 Get 得到的对象有任何假设,更好的做法是归还对象时,将对象清空。
  4. Pool 里对象的生命周期受 GC 影响,不适合于做连接池,因为连接池需要自己管理对象的生命周期。

一些设计思想或者相关知识点:无锁编程、原子操作代替锁、cacheline false sharing 问题、noCopy 禁止复制、分段锁、victim cache 等。

打 赏