Golang中Sync.pool.Get()是随机工作的吗?(50行代码解析)

Golang中Sync.pool.Get()是随机工作的吗?(50行代码解析) 以下示例代码简单地展示了池(pool)的工作原理。

package main

import (
	"fmt"
	"math/rand"
	"runtime"
	"sync"
)

type Data struct {
	tag    string
	buffer []int
}

func main() {
	runtime.GOMAXPROCS(runtime.NumCPU())

	pool := sync.Pool{
		New: func() interface{} {
			data := new(Data)
			data.tag = "new"
			data.buffer = make([]int, 10)
			return data
		},
	}

	wg := new(sync.WaitGroup)
	for i := 0; i < 3; i++ {
		wg.Add(1)
		go func() {
			defer wg.Done()
			data := pool.Get().(*Data)

			for index := range data.buffer {
				data.buffer[index] = rand.Intn(100)
			}
			fmt.Println(data)
			data.tag = "used"
			pool.Put(data)
		}()
	}

	wg.Wait()
	fmt.Println("done")
	for i := 0; i < 3; i++ {
		data := pool.Get().(*Data)
		fmt.Println(data)
	}

	fmt.Scanln()
}

运行此代码后,会得到一些随机的 data 结构,例如: ❯ go run pool.go &{new [81 87 47 59 81 18 25 40 56 0]} &{new [94 11 62 89 28 74 11 45 37 6]} &{new [95 66 28 58 47 47 87 88 90 15]} done &{used [95 66 28 58 47 47 87 88 90 15]} &{new [0 0 0 0 0 0 0 0 0 0]} &{used [94 11 62 89 28 74 11 45 37 6]}

再运行一次: ❯ go run pool.go &{new [81 87 47 59 81 18 25 40 56 0]} &{new [94 11 62 89 28 74 11 45 37 6]} &{new [95 66 28 58 47 47 87 88 90 15]} done &{used [81 87 47 59 81 18 25 40 56 0]} &{used [94 11 62 89 28 74 11 45 37 6]} &{used [95 66 28 58 47 47 87 88 90 15]}

此时,Get() 函数是随机返回的。这意味着无法保证返回池中的哪个数据。 因此,Sync.Pool 保证了并发访问的安全性,但无法提供选择特定数据的功能。 这就是我对 Sync.Pool 的理解。 但是,我不太了解它的实际使用场景。您能提供一些指导或经验分享吗?

抱歉我的英语表达可能不够好。


更多关于Golang中Sync.pool.Get()是随机工作的吗?(50行代码解析)的实战教程也可以访问 https://www.itying.com/category-94-b0.html

2 回复

池并非设计用作您控制的缓冲区,它作为一种性能优化手段存在,旨在减少频繁堆分配对象对垃圾回收器的压力。Godoc 对 Pool 提供了很好的解释:

Pool 是一组可以单独保存和检索的临时对象。

存储在池中的任何项目都可能在任何时候被自动移除,且不会发出通知。 如果此时池持有唯一引用,该项目可能会被释放。

Pool 可安全地供多个 goroutine 同时使用。

Pool 的目的是缓存已分配但未使用的项目以供后续重用,从而减轻垃圾回收器的压力。也就是说,它使得构建高效、线程安全的空闲列表变得容易。然而,它并不适用于所有空闲列表。

Pool 的一个恰当用途是管理一组临时项目,这些项目在包的并发独立客户端之间静默共享,并可能被重用。Pool 提供了一种在许多客户端之间分摊分配开销的方法。

一个良好使用 Pool 的例子在 fmt 包中,它维护着一个动态大小的临时输出缓冲区存储。该存储会在负载下(当许多 goroutine 正在积极打印时)扩展,并在静止时收缩。

另一方面,作为短生命周期对象一部分维护的空闲列表不适合使用 Pool,因为在这种场景下开销无法很好地分摊。让此类对象实现自己的空闲列表会更高效。

Pool 在首次使用后不得被复制。

我加粗的部分值得您注意,这解释了为什么有时 Pool 不会返回您放回池中的对象,而是返回一个全新的对象,从而导致您所描述的随机行为。

更多关于Golang中Sync.pool.Get()是随机工作的吗?(50行代码解析)的实战系列教程也可以访问 https://www.itying.com/category-94-b0.html


sync.Pool.Get() 不是随机工作的,但它的行为具有不确定性。这是由 Go 调度器的并发特性和 sync.Pool 的内部实现机制共同决定的。

核心机制解析

sync.Pool 为每个 P(处理器)维护一个本地池,并有一个共享池。当调用 Get() 时:

  1. 优先从当前 P 的本地池获取
  2. 如果本地池为空,尝试从其他 P 的池中窃取
  3. 如果都为空,调用 New 函数创建新对象

这种设计是为了减少锁竞争,提高并发性能。

代码示例:展示不确定性

package main

import (
    "fmt"
    "sync"
    "time"
)

func main() {
    pool := &sync.Pool{
        New: func() interface{} {
            return "new-item"
        },
    }
    
    // 预先放入一些数据
    for i := 0; i < 5; i++ {
        pool.Put(fmt.Sprintf("item-%d", i))
    }
    
    // 并发获取,结果顺序不确定
    var wg sync.WaitGroup
    for i := 0; i < 10; i++ {
        wg.Add(1)
        go func(id int) {
            defer wg.Done()
            time.Sleep(time.Duration(id%3) * time.Millisecond)
            item := pool.Get()
            fmt.Printf("goroutine %d got: %v\n", id, item)
            pool.Put(item) // 放回以便其他 goroutine 使用
        }(i)
    }
    wg.Wait()
}

实际使用场景

1. 临时对象池(减少 GC 压力)

var bufferPool = sync.Pool{
    New: func() interface{} {
        return new(bytes.Buffer)
    },
}

func ProcessRequest(data []byte) {
    buf := bufferPool.Get().(*bytes.Buffer)
    buf.Reset() // 重要:重置状态
    buf.Write(data)
    // 处理 buf
    bufferPool.Put(buf)
}

2. 数据库连接临时缓存

var stmtPool = sync.Pool{
    New: func() interface{} {
        stmt, _ := db.Prepare("SELECT * FROM users WHERE id = ?")
        return stmt
    },
}

func GetUser(id int) {
    stmt := stmtPool.Get().(*sql.Stmt)
    defer stmtPool.Put(stmt)
    
    row := stmt.QueryRow(id)
    // 处理结果
}

3. JSON 编码器/解码器复用

var encoderPool = sync.Pool{
    New: func() interface{} {
        return json.NewEncoder(io.Discard)
    },
}

func EncodeToJSON(v interface{}) ([]byte, error) {
    encoder := encoderPool.Get().(*json.Encoder)
    defer encoderPool.Put(encoder)
    
    buf := new(bytes.Buffer)
    encoder.SetEscapeHTML(false)
    encoder.Encode(v)
    return buf.Bytes(), nil
}

重要注意事项

  1. 对象状态重置:从池中取出的对象必须重置
data := pool.Get().(*Data)
data.Reset() // 清除旧数据
  1. 不要假设对象生命周期:池中的对象可能在任何时候被 GC
// 错误:假设对象会一直存在
pool.Put(createExpensiveObject())

// 正确:每次需要时都准备创建新对象
obj := pool.Get()
if obj == nil {
    obj = createExpensiveObject()
}
  1. 适合存储临时、可复用的对象,不适合管理连接池等需要精确控制的对象。

sync.Pool 的核心价值在于减少内存分配和 GC 压力,特别是在高并发场景下。它的不确定性是设计上的权衡,以换取更好的性能。

回到顶部