golang sync.pool对象复用 并发原理 缓存池

golang sync.pool对象复用 并发原理 缓存池

在go http每一次go serve(l)都会构建Request数据结构。在大量数据请求或高并发的场景中,频繁创建销毁对象,会导致GC压力。解决办法之一就是使用对象复用技术。在http协议层之下,使用对象复用技术创建Request数据结构。在http协议层之上,可以使用对象复用技术创建(w,*r,ctx)数据结构。这样即可以回快TCP层读包之后的解析速度,也可也加快请求处理的速度。

先上一个测试:

//测试平台 i5 3.8GHz 4核
    bPool := sync.Pool{
        New: func() interface{} {
            b := make([]byte,1024)
            return &b
        },
    }

    t1 := time.Now().Unix()
    count := 1000000000
    for i:=0;i<count;i++{
        buf := make([]byte,1024)
        _ = buf
    }
    t2 := time.Now().Unix()
    for i:=0;i<count;i++{
        buf := bPool.Get().(*[]byte)
        _ = buf
        //clear buf
        bPool.Put(buf)
    }
    t3 := time.Now().Unix()
    fmt.Println("new:%d s",t2-t1)
    fmt.Println("pool:%d s",t3-t1)

结论是这样的:

new:%d s 21
pool:%d s 396

貌似使用池化,性能弱爆了???这似乎与net/http使用sync.pool池化Request来优化性能的选择相违背。这同时也说明了一个问题,好的东西,如果滥用反而造成了性能成倍的下降。在看过pool原理之后,结合实例,将给出正确的使用方法,并给出预期的效果。

基本用法

sync.Pool是一个协程安全临时对象池。数据结构如下:

type Pool struct {
    noCopy noCopy // type noCopy struct{}

    local     unsafe.Pointer 
    localSize uintptr        

    New func() interface{}
}
type poolLocal struct {  
    private interface{}   // Can be used only by the respective P.  
    shared  []interface{} // Can be used by any P.  
    Mutex                 // Protects shared.  
    pad     [128]byte     // Prevents false sharing.  
} 

local 成员的真实类型是一个 poolLocal 数组,localSize 是数组长度。这涉及到Pool实现,pool为每个P分配了一个对象,P数量设置为runtime.GOMAXPROCS(0)。在并发读写时,goroutine绑定的P有对象,先用自己的,没有去偷其它P的。go语言将数据分散在了各个真正运行的P中,降低了锁竞争,提高了并发能力。

不要习惯性地误认为New是一个关键字,这里的New是Pool的一个字段,也是一个闭包名称。其API:

var pool = &sync.Pool{New:func()interface{}{return NewObject()}}
//池对象最好初始化为全局唯一
pool.Put(x interface{})
pool.Get() interface{}

如果不指定New字段,对象池为空时会返回nil,而不是一个新构建的对象。Get()到的对象是随机的。

    pool := sync.Pool{New: func() interface{} {
        return "empty string"
    }}
    s := "Hello World"
    pool.Put(s)
    fmt.Println(pool.Get())
    fmt.Println(pool.Get())

一个缓存池的例子

type BufPool struct {
    pool sync.Pool
    spliter string
}

func NewBufPool() *BufPool {
    return &BufPool{
        pool:sync.Pool{
            New: func() interface{} {
                return &bytes.Buffer{}
            },
        },
        spliter:" ",
    }
}

func (this *BufPool)JoinString(strs ...string) (res string,err error) {
    if len(strs) == 0 {
        return
    }

    buf := this.pool.Get().(*bytes.Buffer)
    if _,err := buf.WriteString(strs[0]);err!=nil{
        return "",err
    }
    for _,str := range strs[1:] {
        if _,err := buf.WriteString(this.spliter);err!= nil {
            return "",err
        }
        if _,err := buf.WriteString(str);err!= nil {
            return "",err
        }
    }

    res = buf.String()
    buf.Reset()
    this.pool.Put(buf)
    return 
}

原生sync.Pool的问题是,Pool中的对象会被GC清理掉,这使得sync.Pool只适合做简单地对象池,不适合作连接池。

为何不适合作连接池

对象的数量和期限

pool创建时不能指定大小,没有数量限制。pool中对象会被GC清掉,只存在于两次GC之间。实现是pool的init方法注册了一个poolCleanup()函数,这个方法在GC之前执行,清空pool中的所有缓存对象。

池对象Get/Put开销

为使多协程使用同一个POOL。最基本的想法就是每个协程,加锁去操作共享的POOL,这显然是低效的。而进一步改进,类似于ConcurrentHashMap(JDK7)的分Segment,提高其并发性可以一定程度性缓解。

注意到pool中的对象是无差异性的,加锁或者分段加锁都不是较好的做法。go的做法是为每一个绑定协程的P都分配一个子池。每个子池又分为私有池和共享列表。共享列表是分别存放在各个P之上的共享区域,而不是各个P共享的一块内存。协程拿自己P里的子池对象不需要加锁,拿共享列表中的就需要加锁了。

Get对象过程:

  1. goroutine固定到某一个P后,先从当前子池私区拿。并置私有对象为空。
  2. 拿不到再从当前子池共享列表拿,需要加锁。
  3. 仍拿不到从其它子池共享列表拿,需要加锁。
  4. 仍拿不到,sync.pool.New闭包非空,则New一个对象。
  5. 所以最坏的情况下遍历其它P才拿到对象,最大值为MACPROCS。

Put过程:

  1. 固定P中私有对象为空,则放到私有对象。
  2. 否则放入当前子池的共享列表,加锁实现。
  3. 开销为最多一次加锁。

如何解决Get最坏情况遍历所有P才获取得对象呢:

  • 能够设置加锁期间遍历其它P的最大次数,遍历不到就直接创建,减少加锁占用pool的时间。
  • 使各子池共享列表中的对象数量尽量平均化,从而避免最坏的情况发生。

方法1止前sync.pool并没有这样的设置。方法2由于goroutine被分配到哪个P由调度器调度不可控,无法确保其平衡。

由于不可控的GC导致生命周期过短,且池大小不可控,因而不适合作连接池。仅适用于增加对象重用机率,减少GC负担。2

用实验回答篇头的问题

实验1

var bytePool = sync.Pool{
   New: func() interface{} {
      b := make([]byte, 1024)
      return &b
   },
}

func main() {
   runtime.GOMAXPROCS(16)
   a := time.Now().Unix()
   count := 100000000
   // 不使用对象池
   for i := 0; i < 1; i++ {
      for j:=0;j<count;j++{
         obj := make([]byte, 1024)
         _ = obj
      }
   }
   b := time.Now().Unix()

   c := time.Now().Unix()
   // 使用对象池
   for i := 0; i < 1; i++ {
      go func() {
         for j := 0; j < count; j++ {
            obj := bytePool.Get().(*[]byte)
            _ = obj
            bytePool.Put(obj)
         }
      }()
   }
   d := time.Now().Unix()
   fmt.Println("without pool ", b-a, "s")
   fmt.Println("with    pool ", d-c, "s")
}

执行结果:

without pool  2 s
with    pool  0 s

单线程情况下,遍历其它无元素的P,长时间加锁性能低下。启用协程改善。

实验2

var bytePool = sync.Pool{
   New: func() interface{} {
      b := make([]byte, 1024)
      return &b
   },
}

func main() {
   runtime.GOMAXPROCS(16)
   a := time.Now().Unix()
   count := 100000000
   // 不使用对象池
   for i := 0; i < 1000; i++ {
      for j:=0;j<count;j++{
         obj := make([]byte, 1024)
         _ = obj
      }
   }
   b := time.Now().Unix()

   c := time.Now().Unix()
   // 使用对象池
   for i := 0; i < 1000; i++ {
      go func() {
         for j := 0; j < count; j++ {
            obj := bytePool.Get().(*[]byte)
            _ = obj
            bytePool.Put(obj)
         }
      }()
   }
   d := time.Now().Unix()
   fmt.Println("without pool ", b-a, "s")
   fmt.Println("with    pool ", d-c, "s")
}

结果:

without pool  2000 s
with    pool  2 s

测试场景在goroutines远大于GOMAXPROCS情况下,与非池化性能差异巨大。

实验3

var bytePool1 = sync.Pool{
    New: func() interface{} {
        b := make([]byte, 1024)
        return &b
    },
}

var bytePool2 = sync.Pool{
    New: func() interface{} {
        b := make([]byte, 1024)
        return &b
    },
}

func main() {
    runtime.GOMAXPROCS(4)
    a := time.Now().Unix()
    count := 1000000000
    goCount := 1000
    for i := 0; i < goCount; i++ {
        go func() {
            for j := 0; j < count; j++ {
                obj := bytePool1.Get().(*[]byte)
                _ = obj
                bytePool1.Put(obj)
            }
        }()
    }
    b := time.Now().Unix()

    for i := 0; i < 1000; i++ {
        go func() {
            for j := 0; j < count; j++ {
                bNew := make([]byte, 1024)
                bytePool2.Put(&bNew)
            }
        }()
    }
    c := time.Now().Unix()
    for i := 0; i < goCount; i++ {
        go func() {
            for j := 0; j < count; j++ {
                obj := bytePool2.Get().(*[]byte)
                _ = obj
                bytePool2.Put(obj)
            }
        }()
    }
    d := time.Now().Unix()
    fmt.Println("without pool ", b-a, "s")
    fmt.Println("with    pool ", d-c, "s")
}

测试结果

without pool  6 s
with    pool  0 s

可以看到同样使用*sync.pool,较大池大小的命中率较高,性能远高于空池。

结论:pool在一定的使用条件下提高并发性能,条件1是协程数远大于GOMAXPROCS,条件2是池中对象远大于GOMAXPROCS。归结成一个原因就是使对象在各个P中均匀分布。

关于何时回收Pool

池pool和缓存cache的区别。池的意思是,池内对象是可以互换的,不关心具体值,甚至不需要区分是新建的还是从池中拿出的。缓存指的是KV映射,缓存里的值互不相同,清除机制更为复杂。缓存清除算法如LRU、LIRS缓存算法。

池空间回收的几种方式。一些是GC前回收,一些是基于时钟或弱引用回收。最终确定在GC时回收Pool内对象,即不回避GC。用java的GC解释弱引用。GC的四种引用:强引用、弱引用、软引用、虚引用。虚引用即没有引用,弱引用GC但有空间则保留,软引用GC即清除。ThreadLocal的值为弱引用的例子。

Pool其它场景

regexp包为了保证并发时使用同一个正则,而维护了一组状态机。

fmt包做字串拼接,从sync.pool拿[]byte对象。避免频繁构建再GC效率高很多。

var ppFree = sync.Pool{
    New: func() interface{} { return new(pp) },
}

https://blog.csdn.net/qq_33339479/article/details/64116948

https://blog.csdn.net/bravezhe/article/details/79887514

https://www.cnblogs.com/hetonghai/p/9086788.html

https://www.jianshu.com/p/2bd41a8f2254

https://www.cnblogs.com/hump/p/6285627.html

https://www.cnblogs.com/DaBing0806/p/6934318.html

https://studygolang.com/articles/700

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,242评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,769评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,484评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,133评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,007评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,080评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,496评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,190评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,464评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,549评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,330评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,205评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,567评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,889评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,160评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,475评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,650评论 2 335

推荐阅读更多精彩内容