深度解密Go语言之sync.pool

最近在工作中碰到了 GC 的问题:项目中大量重复地创建许多对象,造成 GC 的工作量巨大,CPU 频繁掉底。准备使用 sync.Pool 来缓存对象,减轻 GC 的消耗。为了用起来更顺畅,我特地研究了一番,形成此文。本文从使用到源码解析,循序渐进,一一道来。

本文基于 Go 1.14

  • 是什么

  • 有什么用

  • 怎么用

    • 简单的例子

    • fmt 包如何用

    • pool_test

    • 其他

  • 源码分析

    • Pool 结构体

    • Get

    • Put

    • pack/unpack

    • GC

  • 总结

  • 参考资料

是什么

sync.Pool 是 sync 包下的一个组件,可以作为保存临时取还对象的一个“池子”。个人觉得它的名字有一定的误导性,因为 Pool 里装的对象可以被无通知地被回收,可能 sync.Cache 是一个更合适的名字。

有什么用

对于很多需要重复分配、回收内存的地方,sync.Pool 是一个很好的选择。频繁地分配、回收内存会给 GC 带来一定的负担,严重的时候会引起 CPU 的毛刺,而 sync.Pool 可以将暂时不用的对象缓存起来,待下次需要的时候直接使用,不用再次经过内存分配,复用对象的内存,减轻 GC 的压力,提升系统的性能。

怎么用

首先,sync.Pool 是协程安全的,这对于使用者来说是极其方便的。使用前,设置好对象的 New 函数,用于在 Pool 里没有缓存的对象时,创建一个。之后,在程序的任何地方、任何时候仅通过 Get()Put() 方法就可以取、还对象了。

下面是 2018 年的时候,《Go 夜读》上关于 sync.Pool 的分享,关于适用场景:

当多个 goroutine 都需要创建同⼀个对象的时候,如果 goroutine 数过多,导致对象的创建数⽬剧增,进⽽导致 GC 压⼒增大。形成 “并发⼤-占⽤内存⼤-GC 缓慢-处理并发能⼒降低-并发更⼤”这样的恶性循环。

在这个时候,需要有⼀个对象池,每个 goroutine 不再⾃⼰单独创建对象,⽽是从对象池中获取出⼀个对象(如果池中已经有的话)。

因此关键思想就是对象的复用,避免重复创建、销毁,下面我们来看看如何使用。

简单的例子

首先来看一个简单的例子:

package main
import (
  "fmt"
  "sync"
)


var pool *sync.Pool


type Person struct {
  Name string
}


func initPool() {
  pool = &sync.Pool {
    New: func()interface{} {
      fmt.Println("Creating a new Person")
      return new(Person)
    },
  }
}


func main() {
  initPool()


  p := pool.Get().(*Person)
  fmt.Println("首次从 pool 里获取:", p)


  p.Name = "first"
  fmt.Printf("设置 p.Name = %s\n", p.Name)


  pool.Put(p)


  fmt.Println("Pool 里已有一个对象:&{first},调用 Get: ", pool.Get().(*Person))
  fmt.Println("Pool 没有对象了,调用 Get: ", pool.Get().(*Person))
}

运行结果:

Creating a new Person
首次从 pool 里获取:&{}
设置 p.Name = first
Pool 里已有一个对象:&{first},Get:  &{first}
Creating a new Person
Pool 没有对象了,Get:  &{}

首先,需要初始化 Pool,唯一需要的就是设置好 New 函数。当调用 Get 方法时,如果池子里缓存了对象,就直接返回缓存的对象。如果没有存货,则调用 New 函数创建一个新的对象。

另外,我们发现 Get 方法取出来的对象和上次 Put 进去的对象实际上是同一个,Pool 没有做任何“清空”的处理。但我们不应当对此有任何假设,因为在实际的并发使用场景中,无法保证这种顺序,最好的做法是在 Put 前,将对象清空。

fmt 包如何用

这部分主要看 fmt.Printf 如何使用:

func Printf(format string, a ...interface{}) (n int, err error) {
  return Fprintf(os.Stdout, format, a...)
}

继续看 Fprintf

func Fprintf(w io.Writer, format string, a ...interface{}) (n int, err error) {
  p := newPrinter()
  p.doPrintf(format, a)
  n, err = w.Write(p.buf)
  p.free()
  return
}

Fprintf 函数的参数是一个 io.WriterPrintf 传的是 os.Stdout,相当于直接输出到标准输出。这里的 newPrinter 用的就是 Pool:

// newPrinter allocates a new pp struct or grabs a cached one.
func newPrinter() *pp {
  p := ppFree.Get().(*pp)
  p.panicking = false
  p.erroring = false
  p.wrapErrs = false
  p.fmt.init(&p.buf)
  return p
}


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

回到 Fprintf 函数,拿到 pp 指针后,会做一些 format 的操作,并且将 p.buf 里面的内容写入 w。最后,调用 free 函数,将 pp 指针归还到 Pool 中:

// free saves used pp structs in ppFree; avoids an allocation per invocation.
func (p *pp) free() {
  if cap(p.buf) > 64<<10 {
    return
  }


  p.buf = p.buf[:0]
  p.arg = nil
  p.value = reflect.Value{}
  p.wrappedErr = nil
  ppFree.Put(p)
}

归还到 Pool 前将对象的一些字段清零,这样,通过 Get 拿到缓存的对象时,就可以安全地使用了。

pool_test

通过 test 文件学习源码是一个很好的途径,因为它代表了“官方”的用法。更重要的是,测试用例会故意测试一些“坑”,学习这些坑,也会让自己在使用的时候就能学会避免。

pool_test 文件里共有 7 个测试,4 个 BechMark。

TestPoolTestPoolNew 比较简单,主要是测试 Get/Put 的功能。我们来看下 TestPoolNew

func TestPoolNew(t *testing.T) {
  // disable GC so we can control when it happens.
  defer debug.SetGCPercent(debug.SetGCPercent(-1))


  i := 0
  p := Pool{
    New: func() interface{} {
      i++
      return i
    },
  }
  if v := p.Get(); v != 1 {
    t.Fatalf("got %v; want 1", v)
  }
  if v := p.Get(); v != 2 {
    t.Fatalf("got %v; want 2", v)
  }


  // Make sure that the goroutine doesn't migrate to another P
  // between Put and Get calls.
  Runtime_procPin()
  p.Put(42)
  if v := p.Get(); v != 42 {
    t.Fatalf("got %v; want 42", v)
  }
  Runtime_procUnpin()


  if v := p.Get(); v != 3 {
    t.Fatalf("got %v; want 3", v)
  }
}

首先设置了 GC=-1,作用就是停止 GC。那为啥要用 defer?函数都跑完了,还要 defer 干啥。注意到,debug.SetGCPercent 这个函数被调用了两次,而且这个函数返回的是上一次 GC 的值。因此,defer 在这里的用途是还原到调用此函数之前的 GC 设置,也就是恢复现场。

接着,调置了 Pool 的 New 函数:直接返回一个 int,变且每次调用 New,都会自增 1。然后,连续调用了两次 Get 函数,因为这个时候 Pool 里没有缓存的对象,因此每次都会调用 New 创建一个,所以第一次返回 1,第二次返回 2。

然后,调用 Runtime_procPin() 防止 goroutine 被强占,目的是保护接下来的一次 Put 和 Get 操作,使得它们操作的对象都是同一个 P 的“池子”。并且,这次调用 Get 的时候并没有调用 New,因为之前有一次 Put 的操作。

最后,再次调用 Get 操作,因为没有“存货”,因此还是会再次调用 New 创建一个对象。

TestPoolGCTestPoolRelease 则主要测试 GC 对 Pool 里对象的影响。这里用了一个函数,用于计数有多少对象会被 GC 回收:

runtime.SetFinalizer(v, func(vv *string) {
  atomic.AddUint32(&fin, 1)
})

当垃圾回收检测到 v 是一个不可达的对象时,并且 v 又有一个关联的 Finalizer,就会另起一个 goroutine 调用设置的 finalizer 函数,也就是上面代码里的参数 func。这样,就会让对象 v 重新可达,从而在这次 GC 过程中不被回收。之后,解绑对象 v 和它所关联的 Finalizer,当下次 GC 再次检测到对象 v 不可达时,才会被回收。

TestPoolStress 从名字看,主要是想测一下“压力”,具体操作就是起了 10 个 goroutine 不断地向 Pool 里 Put 对象,然后又 Get 对象,看是否会出错。

TestPoolDequeueTestPoolChain,都调用了 testPoolDequeue,这是具体干活的。它需要传入一个 PoolDequeue 接口:

// poolDequeue testing.
type PoolDequeue interface {
  PushHead(val interface{}) bool
  PopHead() (interface{}, bool)
  PopTail() (interface{}, bool)
}

PoolDequeue 是一个双端队列,可以从头部入队元素,从头部和尾部出队元素。调用函数时,前者传入 NewPoolDequeue(16),后者传入 NewPoolChain(),底层其实都是 poolDequeue 这个结构体。具体来看 testPoolDequeue 做了什么:

双端队列

总共起了 10 个 goroutine:1 个生产者,9 个消费者。生产者不断地从队列头 pushHead 元素到双端队列里去,并且每 push 10 次,就 popHead 一次;消费者则一直从队列尾取元素。不论是从队列头还是从队列尾取元素,都会在 map 里做标记,最后检验每个元素是不是只被取出过一次。

剩下的就是 Benchmark 测试了。第一个 BenchmarkPool 比较简单,就是不停地 Put/Get,测试性能。

BenchmarkPoolSTW

  • 6
    点赞
  • 15
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
以下是Go语言实现RSA加密解密的示例代码: ```go package main import ( "crypto/rand" "crypto/rsa" "crypto/x509" "encoding/pem" "fmt" ) func main() { // 生成RSA密钥对 privateKey, err := rsa.GenerateKey(rand.Reader, 2048) if err != nil { panic(err) } // 将私钥编码为PEM格式 privatePem := pem.EncodeToMemory(&pem.Block{ Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(privateKey), }) // 将公钥编码为PEM格式 publicPem, err := x509.MarshalPKIXPublicKey(&privateKey.PublicKey) if err != nil { panic(err) } publicPemBlock := &pem.Block{ Type: "PUBLIC KEY", Bytes: publicPem, } publicPemBytes := pem.EncodeToMemory(publicPemBlock) // 显示密钥对 fmt.Println("Private Key:") fmt.Println(string(privatePem)) fmt.Println("Public Key:") fmt.Println(string(publicPemBytes)) // 加密明文 plaintext := []byte("Hello, world!") ciphertext, err := rsa.EncryptPKCS1v15(rand.Reader, &privateKey.PublicKey, plaintext) if err != nil { panic(err) } fmt.Println("Ciphertext:", ciphertext) // 解密密文 decrypted, err := privateKey.Decrypt(nil, ciphertext, &rsa.OAEPOptions{Hash: rsa.HashSHA256}) if err != nil { panic(err) } fmt.Println("Decrypted:", string(decrypted)) } ``` 在上述示例中,首先使用`rsa.GenerateKey()`函数生成了一个2048位的RSA密钥对。然后,使用`x509.MarshalPKCS1PrivateKey()`和`x509.MarshalPKIXPublicKey()`函数将私钥和公钥编码为PEM格式,并将它们打印出来。 接着,使用`rsa.EncryptPKCS1v15()`函数对明文进行加密,得到密文。使用私钥的`Decrypt()`方法对密文进行解密,得到原始的明文。 需要注意的是,在进行加密和解密时,需要使用不同的密钥。在本例中,使用公钥加密明文,然后使用私钥解密密文。如果使用私钥加密明文,那么只有对应的公钥才能解密密文。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值