65

我所理解的Sync Pool

 4 years ago
source link: https://www.tuicool.com/articles/yuiuMfv
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

看gin源码时发现了 sync.Pool 的使用

// gin.go:L144
func New() *Engine {
    ...

    engine.pool.New = func() interface{} {
        return engine.allocateContext()
    }
    return engine
}

// gin.go: L346
func (engine *Engine) ServeHTTP(w http.ResponseWriter, req *http.Request) {
    c := engine.pool.Get().(*Context)
    c.writermem.reset(w)
    c.Request = req
    c.reset()

    engine.handleHTTPRequest(c)

    engine.pool.Put(c)
}

那个时候其实不太明白这个 Pool 是在干啥用, 大致觉得应该是内存池之类的. 后面想仔细看下 sync.Pool 具体怎么用, 我就去直接看了下Pool的源码, 然后直接懵逼了

因为基本看不懂其逻辑, 因为Pool的源码涉及到Golang的调度相关的知识. 要是不明白Golang是如何调度的, 基本是看不懂Pool的源码的(虽说它只有短短的259行代码, 文件路径: SDK/src/sync/pool.go). 这里推荐Go调度器系列 系列文章

后面我就去看看别人是怎么理解和使用Pool的, 我就搜到了这么一篇文章 go语言的官方包sync.Pool的实现原理和适用场景 里面有这么一个例子

package main

import(
    "fmt"
    "sync"
)

func main() {
    p := &sync.Pool{
        New: func() interface{} {
            return 0
        },
    }

    a := p.Get().(int)
    p.Put(1)
    b := p.Get().(int)
    fmt.Println(a, b)
}

以及Golang中国论坛上一个人的 提问 , 我不禁陷入了深深的疑问, 这个Golang的Pool到底是用来做什么的?

我疑问的地方:

  1. sync.Pool的运用场景是什么, 哪些地方能用到Pool, 哪些地方不能用?
  2. 为啥CSDN上先Put后Get, 就能拿到1; 而Golang中国上那个提问, 为啥得到的顺序就不确定了?

我后面想了很久, 以及再回头去看 gin , logrus 的源码, 我才想明白: 这两个例子都是"坑货". 这两个例子都是sync.Pool使用的反面例子, 都是不正确的用法. 其实这些在源码的注释中, 都是由明确说明的, 只是当时没能理解. Callers should not assume any relation between values passed to Put and the values returned by Get . 简单来说: 就是 GetPut 没有任何关系, 我们用Pool的时候, 要时刻记得这个.

因此, 我的 疑问2 就迎刃而解了, 其实就是这个一直困惑着我

关于 疑问1 就更简单了

// Pool's purpose is to cache allocated but unused items for later reuse,
// relieving pressure on the garbage collector. That is, it makes it easy to
// build efficient, thread-safe free lists. However, it is not suitable for all
// free lists.

简单来说, Pool就是为了减少GC压力的, 重复利用内存. 千万不能把他当成内存池使用

其实Pool的用法很简单, 就是先 Get , 用完之后 Put , 如 gin 的使用.

再比如 logrus 的用法

func (logger *Logger) Println(args ...interface{}) {
    entry := logger.newEntry()
    entry.Println(args...)
    logger.releaseEntry(entry)
}

func (logger *Logger) newEntry() *Entry {
    entry, ok := logger.entryPool.Get().(*Entry)
    if ok {
        return entry
    }
    return NewEntry(logger)
}

func (logger *Logger) releaseEntry(entry *Entry) {
    entry.Data = map[string]interface{}{}
    logger.entryPool.Put(entry)
}

所以别被别的池子带跑了, golang里的sync.Pool就是GC优化的, 用法很简单

gocn 有这么一个问题: 想在pool的基础上做一个限制池中对象数量的功能, 发现还是多次执行pool.New. 期望是只执行一次NewBuffer,也就是只打印一次alloc。 实际上每次执行,会打印多次alloc

const MaxFrameSize = 5000

func main() {
    for i := 0; i < 10; i++ {
        // 多个协程想从pool中拿到对象
        go func() {
            c := getBuf()
            putBuf(c)
            log.Println("put done")
        }()
    }

    time.Sleep(3 * time.Second)
}

var bufPool = sync.Pool{
    New: func() interface{} {
        log.Println("alloc")
        return bytes.NewBuffer(make([]byte, 0, MaxFrameSize))
    },
}

var bufPoolChan = make(chan bool, 1)

func getBuf() *bytes.Buffer {
    bufPoolChan <- true
    b := bufPool.Get().(*bytes.Buffer)
    b.Reset()
    return b
}

func putBuf(b *bytes.Buffer) {
    bufPool.Put(b)
    <-bufPoolChan
}

大神解答:

sync.Pool的源代码里说了,pool里的对象随时都有可能被自动移除,并且没有任何通知。sync.Pool的数量是不可控制的。

Pool调用New与线程调度有关,Pool内部有一个localPool的数组,每个P对应其中一个localPool,在当前P执行goroutine的时候,优先从当前的localPool的private变量取,娶不到在从shared列表里面取,再取不到就尝试从别的P的localPool的shared里面偷一个。最后实在取不到就New一个。

由于你的bufPoolChan限制基本上10个goroutine就在两个P后面排队轮流执行,所以alloc就会出现两次,后面的基本就是从这两个localPool的private取出来的。

如果取消这个限制,10个goroutine很快就被分配到10个P上去了,对应就有10个localPool,10次每次取private都取不到,取shared列表也取不到,别的localPool也没得偷,就会New10次,alloc就会出现10次。

其他高级用法, 后面再补充


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK