Go 并发系列是根据我对晁岳攀老师的《Go 并发编程实战课》的吸取和理解整理而成,若有误差,欢迎指正~
Go 是一个支持自动垃圾回收的语言,对程序员而言,咱们想建立对象就建立,不用关心资源的回收,大大提升了开发的效率。程序员
可是方便的背后,却有也有不小的代价。Go 的垃圾回收机制仍是有一个 STW (stop-the-world,程序暂停)的时间,大量建立的对象,都会影响垃圾回收标记的时间。数据库
除此以外,像数据库的链接,tcp 链接,这些链接的建立自己就十分耗时,若是能将这些链接复用,也能减小业务耗时。编程
因此,高并发场景下,采用池化(Pool)手段,对某些对象集中管理,重复利用,减小建立和垃圾回收的成本,不只能够大大提升业务的响应速度,也能提升程序的总体性能。缓存
池化就是对某些对象进行集中管理,重复利用,减小对象建立和垃圾回收的成本。安全
Go 标准库 sync 提供了一个通用的 Pool,经过这个 Pool 能够建立池化对象,实现通常对象的管理。数据结构
下面咱们主要看一下 sync.Pool 的实现。并发
sync.Pool 的使用很简单,它有1个对外的成员变量 New 和2个对外的成员方法 Get 和 Put。tcp
下面是一个使用示例(见 fUsePool 方法):函数
type AFreeCoder struct { officialAccount string article string content \[\]string placeHolder string}// 为了真实模拟,这里禁止编译器使用内联优化//go:noinlinefunc NewAFreeCoder() \*AFreeCoder { return &AFreeCoder{ officialAccount: "码农的自由之路", content: make(\[\]string, 10000, 10000), placeHolder: "若是以为有用,欢迎关注哦~", }}func (a \*AFreeCoder) Write() { a.article = "Go 并发之性能提高杀器 Pool"}func f(concurrentNum int) { var w sync.WaitGroup w.Add(concurrentNum) for i := 0; i < concurrentNum; i++ { go func() { defer w.Done() a := NewAFreeCoder() a.Write() }() } w.Wait()}func fUsePool(concurrentNum int) { var w sync.WaitGroup p := sync.Pool{ New: func() interface{} { return NewAFreeCoder() }, } w.Add(concurrentNum) for i := 0; i < concurrentNum; i++ { go func() { defer w.Done() a := p.Get().(\*AFreeCoder) defer p.Put(a) a.Write() }() } w.Wait()}
AFreeCoder 是自定义的结构体,用来模拟初始化比较耗时类型。高并发
New 是函数类型变量,传入的函数须要实现 AFreeCoder 的初始化。
Get 方法返回的是 interface{} 类型,须要断言成 New 返回的类型。
Put 方法也比较好理解,变量用完了再放回去。
上面的示例中,f 和 fUsePool 分别实现了不使用 Pool 和使用 Pool 状况下,并发执行 Write 函数的功能。
那么这两个函数的性能对好比何呢?咱们能够用 go test 的 benchmark 测试一下(并发数 concurrentNum=100),测试结果以下:
goos: darwingoarch: amd64pkg: go\_practice/pool\_exampleBenchmark\_f-8 853 1355041 ns/op 16392237 B/op 203 allocs/opBenchmark\_fUsePool-8 12460 98046 ns/op 565066 B/op 9 allocs/opPASSok go\_practice/pool\_example 4.663s
测试结果显示,使用了 Pool 以后,内存分配的次数相比不使用 Pool 的方式少不少,总体的耗时也会小不少。
若是把上面示例中初始化函数 NewAFreeCoder 中 content 的初始化操做去掉,再测试一次呢?测试结果以下:
goos: darwingoarch: amd64pkg: go\_practice/pool\_exampleBenchmark\_f-8 853 1355041 ns/op 16392237 B/op 203 allocs/opBenchmark\_fUsePool-8 12460 98046 ns/op 565066 B/op 9 allocs/opPASSok go\_practice/pool\_example 4.663s
上面数据粘错了)使用了 Pool 以后,内存分配次数和每次操做消耗的内存仍然不多,可是总体的耗时相对不使用 Pool 的状况并没减小。
这是由于此时建立 AFreeCoder 对象的成本较低,而 Pool 相关操做也会有性能的消耗,因此才致使二者总体耗时差很少。
sync.Pool 自己是线程安全的,能够多个 goroutine 并发调用,使用起来很方便,可是有两个注意点:
第1点,禁止拷贝很好理解,毕竟 New 很容易修改。
第2点,不能存放须要保持长链接的对象。这是由于 sync.Pool 注册了本身的 Pool 清理函数,Pool 中的变量可能会被垃圾回收掉。若是需求保存长链接,有不少其它的 Pool 实现了这种功能。
看一下 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 optionally specifies a function to generate // a value when Get would otherwise return nil. // It may not be changed concurrently with calls to Get. New func() interface{} }
noCopy 不用多解释,用于静态检查,防拷贝的。New 前面也说过,用来存对象初始化函数的。
重点是 local 和 victim 这两个字段。解释这两个字段前,先上一张《Go 并发实战课》原文的 sync.Pool 数据结构的示意图:
sync.Pool 中,缓存的对象并非存储在一个队列中,而是根据处理器 P 的核数 n 存了 n 份,这样能最大程度的保证并发的时候 n 个 goroutine 能够同时获取对象。
local 和 victim 结构都同样,都是 poolLocal 类型,有 private 和 shared 成员。private 存储单个对象,shared 是 poolChain 类型,相似队列,存了一堆对象。由于 local 和 victim 都是和处理器绑定的,当某个 goroutine 独占一个处理器时,直接经过 private 取值不须要加锁,速度就会很快。
为何有了 local,还须要 victim 呢?这是为了下降池子中对象被回收的可能性。
因为 sync.Pool 中存储对象的个数不定,大小不定,因此它须要在系统闲暇的时候将变量回收掉。其实现方式以下:
func poolCleanup() { for \_, p := range oldPools { p.victim = nil p.victimSize = 0 } for \_, p := range allPools { p.victim = p.local p.victimSize = p.localSize p.local = nil p.localSize = 0 } oldPools, allPools = allPools, nil }
poolClean() 函数被注册到了 runtime 中,会在每一次 GC 调用以前被调用。这样 GC 第一次调用的时候,local 虽然被清空,可是还能经过 victim 拿到池子中的对象。
Put 方法实现的功能是将用完的对象从新放回池子里。由于 Put 比较简单,因此先介绍 Put 方法。
Put 方法实现以下:
func (p \*Pool) Put(x interface{}) { if x == nil { return } // 把当前goroutine固定在当前的P上 // l 就是 local l, \_ := p.pin() if l.private == nil { l.private = x x = nil } if x != nil { l.shared.pushHead(x) } runtime\_procUnpin()}
先说一下 p.pin() 和 runtime\_procUnpin(), 这两个函数分别实现了某 goroutine 抢占当前 P(处理器)和解除抢占的功能。因此这里 private 的复制和以后 Get 方法中的读取都不须要加锁。
整个逻辑比较简单,优先存到本地 private,若是 private 已经有值了,就放到本地队列中。
Get 方法实现以下:
func (p \*Pool) Get() interface{} { // 把当前goroutine固定在当前的P上 l, pid := p.pin() x := l.private // 优先从local的private字段取,快速 l.private = nil if x == nil { // 从当前的local.shared弹出一个,注意是从head读取并移除 x, \_ = l.shared.popHead() if x == nil { // 若是没有,则去偷一个 x = p.getSlow(pid) } } runtime\_procUnpin() // 若是没有获取到,尝试使用New函数生成一个新的 if x == nil && p.New != nil { x = p.New() } return x}
Get 方法总体归纳就是从池子中取出一个对象,若是没有对象了,就 New 一个,再返回。
细节上,先从当前 P 对应的 local 的 private 获取,获取不到,就从当前 P 的 local 的队列 shared 中获取,还获取不到就从其它 P 的 shared 中获取 (getSlow 方法)。
若是最终仍然获取不到,才 New 一个对象。
虽然 sync.Pool 也作了不少优化,性能有了很大的提高,可是使用的时候仍是有两个坑:
内存泄露
若是池子中对象的类型是 slice,它的 cap 可能不断的变大,而 sync.Pool 的回收机制(第二次回收)可能致使这些过大的对象愈来愈多,且一直没法回收,最终形成内存泄露。
因此有一些特定 Pool 的使用中,会对池子中的变量的大小作一个限制,超过一个阈值直接丢弃。
内存浪费
除了内存泄露外,还有一种浪费的状况,就是池子中的变量变得很大,可是不少时候只须要一个很小的变量,就会形成内存浪费的状况。
由于 sync.Pool 保存的对象可能会被无通知的释放掉,并不适合用来保存链接对象。链接对象的保存通常都经过其它方法完成。
好比 Go 中 http 链接的链接池的实如今 Transport 中,它用一个 idleConn 对象(map)来保存链接,因为没有相似 sync.Pool 的垃圾回收方法 PoolClean(),因此能保持长链接。Transport 对链接数量的控制经过 LRU 实现。
像第三方包 faith/pool,它是经过 channel + Mutex 的方式实现的 Pool,空闲的链接放到 channel 中。这也是 channel 的一个应用场景。
Pool 是一个通用的概念,也是解决对象重用和预先分配的一个经常使用的优化手段。
可是项目一开始其实不必考虑考虑这种优化,只有到了中后期阶段,出现性能瓶颈,须要优化的时候,能够考虑经过 Pool 的方式来优化。
码农的自由之路
996的码农,也能自由~
47篇原创内容
公众号
都看到这里了,不如点个 赞/在看,加个关注呗~~