天天看点

Go语言标准库学习之sync二(通过sync.Pool大幅度提升程序运行性能)

sync.pool对象在并发比较高的系统中是非常常见的,这篇博客向大家介绍sync包创建可复用的实例池的原理以及使用方法,希望对你有帮助。

1. 池的内部工作流程

我们调用sync.pool包中的方法的时候,init函数会自动注册一个清理pool对象的方法,该方法会在GC执行前被调用,所以Pool会在调用GC的时候性能较低(初始化的对象都被清理了,重新创建就会产生开销)。Pool只在两次GC之间是有效的,下面是官方的一张图,用来理解池的管理方式:

Go语言标准库学习之sync二(通过sync.Pool大幅度提升程序运行性能)

对于我们创建的每个

sync.Pool

,go 生成一个连接到每个处理器(处理器即 Go 中调度模型 GMP 的 P,pool 里实际存储形式是

[P]poolLocal

)的内部池

poolLocal

。该结构由两个属性组成:

  • private

    能由其所有者访问(push 和 pop 不需要任何锁)
  • shared

    该属性可由任何其他处理器读取,并且需要并发安全。

实际上,池不是简单的本地缓存,它可以被我们的应用程序中的任何 线程/goroutines 使用。

2. 两个重要方法

  • Get方法
    // 1)尝试从本地P对应的那个本地池中获取一个对象值, 并从本地池冲删除该值。
    // 2)如果获取失败,那么从共享池中获取, 并从共享队列中删除该值。
    // 3)如果获取失败,那么从其他P的共享池中偷一个过来,并删除共享池中的该值(p.getSlow())。
    // 4)如果仍然失败,那么直接通过New()分配一个返回值,注意这个分配的值不会被放入池中。New()返回用户注册的New函数的值,如果用户未注册New,那么返回nil。
    func (p *Pool) Get() interface{} {
    	if race.Enabled {
    		race.Disable()
    	}
    	l := p.pin()
    	x := l.private
    	l.private = nil
    	runtime_procUnpin()
    	if x == nil {
    		l.Lock()
    		last := len(l.shared) - 1
    		if last >= 0 {
    			x = l.shared[last]
    			l.shared = l.shared[:last]
    		}
    		l.Unlock()
    		if x == nil {
    			x = p.getSlow()
    		}
    	}
    	if race.Enabled {
    		race.Enable()
    		if x != nil {
    			race.Acquire(poolRaceAddr(x))
    		}
    	}
    	if x == nil && p.New != nil {
    		x = p.New()
    	}
    	return x
    }
               
  • Put方法
    // 将x添加到池里面
    // 1)如果放入的值为空,直接return.
    // 2)检查当前goroutine的是否设置对象池私有值,如果没有则将x赋值给其私有成员,并将x设置为nil。
    // 3)如果当前goroutine私有值已经被设置,那么将该值追加到共享列表。
    func (p *Pool) Put(x interface{}) {
    	if x == nil {
    		return
    	}
    	if race.Enabled {
    		if fastrand()%4 == 0 {
    			// Randomly drop x on floor.
    			return
    		}
    		race.ReleaseMerge(poolRaceAddr(x))
    		race.Disable()
    	}
    	l := p.pin()
    	if l.private == nil {
    		l.private = x
    		x = nil
    	}
    	runtime_procUnpin()
    	if x != nil {
    		l.Lock()
    		l.shared = append(l.shared, x)
    		l.Unlock()
    	}
    	if race.Enabled {
    		race.Enable()
    	}
    }
               

3. pool缓存对象的数量和期限

在put方法中我们可以看到,源码中并没有指定pool的大小,所以sync.Pool的缓存对象数量是没有限制的(只受限于内存)。需要注意的是sync.Pool缓存对象的期限是受GC影响的,sync.Pool在init()中向runtime注册了一个cleanup方法,因此sync.Pool缓存的期限只是两次gc之间这段时间。

func init() {
	runtime_registerPoolCleanup(poolCleanup)
}
           

4. 基准测试

下面我们来测试一下使用Pool对程序性能的影响:

package main

import (
	"sync"
	"testing"
)

type Student struct {
	Age int
}

func BenchmarkWithPool(b *testing.B) {
	var s *Student
	// 调用sync.Pool,初始化Student
	var pool = sync.Pool{
		New: func() interface{} { return new(Student) },
	}
	// 基准测试
	for i := 0; i < b.N; i++ {
		for j := 0; j < 10000; j++ {
			// 从内存池中获取Student
			s = pool.Get().(*Student)
			s.Age = j
			// 使用完将s归还到Pool
			pool.Put(s)
		}
	}
}

// 这个例子我们不使用sync.Pool
func BenchmarkWithNoPool(b *testing.B) {
	var s *Student
	for i := 0; i < b.N; i++ {
		for j := 0; j < 10000; j++ {
			s = &Student{Age: i}
			s.Age++
		}
	}
}

           

测试一下:

$ go test -bench=. -benchmem
goos: windows
goarch: amd64
pkg: test
BenchmarkWithPool-8                10000            182103 ns/op               0 B/op          0 allocs/op
BenchmarkWithNoPool-8              10000            160606 ns/op           80000 B/op      10000 allocs/op
PASS
ok      test    3.771s
           

这里解释一下,测试结果中各字段的含义:

  • 第一列为基准测试的函数名称。
  • 第二列为基准测试的迭代总次数 b.N。
  • 第三列为平均每次迭代所消耗的纳秒数。
  • 第四列为平均每次迭代内存所分配的字节数。
  • 第五列为平均每次迭代的内存分配次数。

我们可以看到不采用Pool和采用Pool两者在内存分配上有很大的差异,使用Pool大大减少了内存的分配,从而程序性能也就大大提高了。

关于sync标准库的使用请参考下面文章:

Go语言标准库学习之sync一(go语言中的锁)