1. sync 包还是 channel

Go 语言提倡 “不要通过共享内存来通信,而应该通过通信来共享内存”。正如在之前的章节中所学的那样,我们建议大家优先使用 CSP 并发模型进行并发程序设计。但是在下面一些场景下,我们依然需要 sync 包提供的低级同步原语。

  • 需要高性能的临界区(critical section)同步机制场景

在 Go 中,channel 属于高级同步原语,其自身的实现也是建构在低级同步原语之上的。因此,channel 自身的性能与低级同步原语相比要略微逊色。因此,在需要高性能的临界区(critical section)同步机制的情况下,sync 包提供的低级同步原语更为适合。下面是 sync.Mutex 和 channel 各自实现的临界区同步机制的一个简单性能对比:

  1. // go-sync-package-1_test.go
  2. package main
  3. import (
  4. "sync"
  5. "testing"
  6. )
  7. var cs = 0 // 模拟临界区要保护的数据
  8. var mu sync.Mutex
  9. var c = make(chan struct{}, 1)
  10. func criticalSectionSyncByMutex() {
  11. mu.Lock()
  12. cs++
  13. mu.Unlock()
  14. }
  15. func criticalSectionSyncByChan() {
  16. c <- struct{}{}
  17. cs++
  18. <-c
  19. }
  20. func BenchmarkCriticalSectionSyncByMutex(b *testing.B) {
  21. for n := 0; n < b.N; n++ {
  22. criticalSectionSyncByMutex()
  23. }
  24. }
  25. func BenchmarkCriticalSectionSyncByChan(b *testing.B) {
  26. for n := 0; n < b.N; n++ {
  27. criticalSectionSyncByChan()
  28. }
  29. }

运行这个对比测试(Go 1.13.6):

  1. $go test -bench . go-sync-package-1_test.go
  2. goos: darwin
  3. goarch: amd64
  4. BenchmarkCriticalSectionSyncByMutex-8 84364287 13.3 ns/op
  5. BenchmarkCriticalSectionSyncByChan-8 26449521 44.4 ns/op
  6. PASS
  7. ok command-line-arguments 2.362s
  • 不想转移结构体对象所有权,但又要保证结构体内部状态数据的同步访问的场景

基于 channel 的并发设计的一个特点就是:在 goroutine 间通过 channel 转移数据对象的所有权。只有拥有数据对象所有权(从 channel 接收到该数据)的 goroutine 才可以对该数据对象进行状态变更。如果你的设计中没有转移结构体对象所有权,但又要保证结构体内部状态数据在多个 goroutine 之间同步访问,那么你可以使用 sync 包提供的低级同步原语来实现,比如最常用的sync.Mutex。

2. sync 包使用的注意事项

在$GOROOT/src/sync/mutex.go文件中,我们看到这样一行关于 sync 包使用的注意事项:

  1. // Values containing the types defined in this package should not be copied.
  2. // 不应复制那些包含了此包中类型的值

在 sync 包的其他源文件中,我们还会看到类似如下的一些注释:

  1. // $GOROOT/src/sync/mutex.go
  2. // A Mutex must not be copied after first use. (禁止复制首次使用后的Mutex)
  3. // $GOROOT/src/sync/rwmutex.go
  4. // A RWMutex must not be copied after first use.(禁止复制首次使用后的RWMutex)
  5. // $GOROOT/src/sync/cond.go
  6. // A Cond must not be copied after first use.(禁止复制首次使用后的Cond)
  7. ... ...

为什么不应对 Mutex 等 sync 包中定义的结构类型在首次使用后进行复制操作呢?我们来看一个例子:

  1. // go-sync-package-2.go
  2. package main
  3. import (
  4. "log"
  5. "sync"
  6. "time"
  7. )
  8. type foo struct {
  9. n int
  10. sync.Mutex
  11. }
  12. func main() {
  13. f := foo{n: 17}
  14. go func(f foo) {
  15. for {
  16. log.Println("g2: try to lock foo...") // 2
  17. f.Lock()
  18. log.Println("g2: lock foo ok") // 3
  19. time.Sleep(3 * time.Second)
  20. f.Unlock()
  21. log.Println("g2: unlock foo ok") // 5
  22. }
  23. }(f)
  24. f.Lock()
  25. log.Println("g1: lock foo ok") // 1
  26. // 在mutex首次使用后复制其值
  27. go func(f foo) {
  28. for {
  29. log.Println("g3: try to lock foo...") // 4 一直阻塞
  30. f.Lock()
  31. log.Println("g3: lock foo ok")
  32. time.Sleep(5 * time.Second)
  33. f.Unlock()
  34. log.Println("g3: unlock foo ok")
  35. }
  36. }(f)
  37. time.Sleep(1000 * time.Second)
  38. f.Unlock()
  39. log.Println("g1: unlock foo ok")
  40. }

运行该示例:

  1. $go run go-sync-package-2.go
  2. 2020/02/08 21:16:46 g1: lock foo ok
  3. 2020/02/08 21:16:46 g2: try to lock foo...
  4. 2020/02/08 21:16:46 g2: lock foo ok
  5. 2020/02/08 21:16:46 g3: try to lock foo...
  6. 2020/02/08 21:16:49 g2: unlock foo ok
  7. 2020/02/08 21:16:49 g2: try to lock foo...
  8. 2020/02/08 21:16:49 g2: lock foo ok
  9. 2020/02/08 21:16:52 g2: unlock foo ok
  10. 2020/02/08 21:16:52 g2: try to lock foo...
  11. 2020/02/08 21:16:52 g2: lock foo ok
  12. ... ...

我们在示例中创建了两个 goroutine:g2 和 g3。示例运行的结果显示:g3 阻塞在加锁操作上了,而 g2 则如预期正常运行。g2 和 g3 的差别就在于 g2 是在互斥锁首次使用之前创建的,而 g3 则是在互斥锁执行完加锁操作并处于锁定状态之后创建的,并且程序在创建 g3 的时候复制了 foo 的实例(包含了 sync.Mutex 的实例)并在之后使用了这个副本。

Go 标准库中 sync.Mutex 的定义如下:

  1. // $GOROOT/src/sync/mutex.go
  2. type Mutex struct {
  3. state int32
  4. sema uint32
  5. }

我们看到 Mutex 的定义非常简单,它由两个字段 state 和 sema 组成:

  • state:表示当前互斥锁的状态。
  • sema:用于控制锁状态的信号量。

对 Mutex 实例的复制即是两个整型字段的复制。初始情况下,Mutex 的实例处于Unlocked状态(state 和 sema 均为 0)。g2 复制了处于初始状态的 Mutex 实例,副本的 state 和 sema 也均为 0,这与 g2 自己定义一个新的 Mutex 实例无异,这也决定了 g2 后续可以按预期正常运行。

后续主程序调用了 Lock 方法,Mutex 的实例变为Locked状态(state 字段值为赋值为sync.mutexLocked),而此后 g3 创建时恰恰复制了处于Locked状态的 Mutex 实例(副本的 state 字段值亦为sync.mutexLocked),因此 g3 再对其实例副本调用 Lock 方法将会导致其进入阻塞状态(也是死锁状态,因为没有任何其他机会调用该副本的 Unlock 方法了,并且 Go 不支持递归锁)。

经过实验, 即使 main goroutine 释放锁后, g3 仍在阻塞! 猜测原因是, 当 f 被传递给 g3 后, 其实就是为 g3 生成了新锁, 但是初始状态是 “锁定”, 所以没有其它 goroutine 释放 g3 的锁, g3 进入死锁!

3. 互斥锁(Mutex)还是读写锁(RWMutex)

那读写锁(RWMutex)究竟适合在哪种场景下应用呢?我们先通过下面示例来对比一下互斥锁和读写锁在不同并发量下的性能数据:

  1. // go-sync-package-3_test.go
  2. package main
  3. import (
  4. "sync"
  5. "testing"
  6. )
  7. var cs1 = 0 // 模拟临界区要保护的数据
  8. var mu1 sync.Mutex
  9. var cs2 = 0 // 模拟临界区要保护的数据
  10. var mu2 sync.RWMutex
  11. func BenchmarkReadSyncByMutex(b *testing.B) {
  12. b.RunParallel(func(pb *testing.PB) {
  13. for pb.Next() {
  14. mu1.Lock()
  15. _ = cs1
  16. mu1.Unlock()
  17. }
  18. })
  19. }
  20. func BenchmarkReadSyncByRWMutex(b *testing.B) {
  21. b.RunParallel(func(pb *testing.PB) {
  22. for pb.Next() {
  23. mu2.RLock()
  24. _ = cs2
  25. mu2.RUnlock()
  26. }
  27. })
  28. }
  29. func BenchmarkWriteSyncByRWMutex(b *testing.B) {
  30. b.RunParallel(func(pb *testing.PB) {
  31. for pb.Next() {
  32. mu2.Lock()
  33. cs2++
  34. mu2.Unlock()
  35. }
  36. })
  37. }

我们分别在 cpu=2, 8,16,32,64, 128 的情况下运行上述并发性能测试,测试结果如下:

  1. $go test -bench . go-sync-package-3_test.go -cpu 2
  2. goos: darwin
  3. goarch: amd64
  4. BenchmarkReadSyncByMutex-2 72718717 16.4 ns/op
  5. BenchmarkReadSyncByRWMutex-2 29053934 41.2 ns/op
  6. BenchmarkWriteSyncByRWMutex-2 38043865 28.7 ns/op
  7. PASS
  8. ok command-line-arguments 3.576s
  9. $go test -bench . go-sync-package-3_test.go -cpu 8
  10. goos: darwin
  11. goarch: amd64
  12. BenchmarkReadSyncByMutex-8 23004751 52.8 ns/op
  13. BenchmarkReadSyncByRWMutex-8 29302923 40.8 ns/op
  14. BenchmarkWriteSyncByRWMutex-8 19118193 61.7 ns/op
  15. PASS
  16. ok command-line-arguments 3.757s
  17. $go test -bench . go-sync-package-3_test.go -cpu 16
  18. goos: darwin
  19. goarch: amd64
  20. BenchmarkReadSyncByMutex-16 20492412 58.8 ns/op
  21. BenchmarkReadSyncByRWMutex-16 29786635 40.9 ns/op
  22. BenchmarkWriteSyncByRWMutex-16 17095704 68.1 ns/op
  23. PASS
  24. ok command-line-arguments 3.768s
  25. $go test -bench . go-sync-package-3_test.go -cpu 32
  26. goos: darwin
  27. goarch: amd64
  28. BenchmarkReadSyncByMutex-32 20217310 63.4 ns/op
  29. BenchmarkReadSyncByRWMutex-32 29373686 40.7 ns/op
  30. BenchmarkWriteSyncByRWMutex-32 14463114 81.6 ns/op
  31. PASS
  32. ok command-line-arguments 3.853s
  33. $go test -bench . go-sync-package-3_test.go -cpu 64
  34. goos: darwin
  35. goarch: amd64
  36. BenchmarkReadSyncByMutex-64 20733363 66.1 ns/op
  37. BenchmarkReadSyncByRWMutex-64 34930328 34.4 ns/op
  38. BenchmarkWriteSyncByRWMutex-64 15703741 82.8 ns/op
  39. PASS
  40. ok command-line-arguments 4.057s
  41. $go test -bench . go-sync-package-3_test.go -cpu 128
  42. goos: darwin
  43. goarch: amd64
  44. BenchmarkReadSyncByMutex-128 19807524 68.2 ns/op
  45. BenchmarkReadSyncByRWMutex-128 29254756 40.8 ns/op
  46. BenchmarkWriteSyncByRWMutex-128 14505304 81.8 ns/op
  47. PASS
  48. ok command-line-arguments 3.936s

通过测试结果对比,我们得到一些结论:

  • 并发量较小的情况下,Mutex 性能最好;随着并发量增大,Mutex 的竞争激烈,导致加锁和解锁性能下降;
  • RWMutex 的读锁性能并未随并发量的增大而发生较大变化,性能始终恒定在 40ns 左右;
  • 在并发量较大的情况下,RWMutex 的写锁性能与 Mutex、RWMutex 读锁相比是最差的,并且随着并发量增大,写锁性能有继续下降趋势。

由此,我们可以看出,读写锁适合应用在具有一定并发量且读多写少的场合。在大量并发读的情况下,多个 goroutine 可以同时持有读锁,从而减少在锁竞争中等待的时间;而互斥锁即便是读请求,同一时刻也只能有一个 goroutine 持有锁,其他 goroutine 只能阻塞在加锁操作上等待被调度。

4. 条件变量

条件变量是同步原语的一种,如果没有条件变量,开发人员可能需要在 goroutine 中通过连续轮询的方式检查是否满足条件,这种连续轮询非常消耗资源,因为 goroutine 在这个过程中处于活动状态但其工作并无进展。下面就是一个用sync.Mutex实现对条件轮询等待的例子:

  1. // go-sync-package-4.go
  2. package main
  3. import (
  4. "fmt"
  5. "sync"
  6. "time"
  7. )
  8. type signal struct{}
  9. var ready bool
  10. func worker(i int) {
  11. fmt.Printf("worker %d: is working...\n", i)
  12. time.Sleep(1 * time.Second)
  13. fmt.Printf("worker %d: works done\n", i)
  14. }
  15. func spawnGroup(f func(i int), num int, mu *sync.Mutex) <-chan signal {
  16. c := make(chan signal)
  17. var wg sync.WaitGroup
  18. for i := 0; i < num; i++ {
  19. wg.Add(1)
  20. go func(i int) {
  21. for {
  22. mu.Lock()
  23. if !ready {
  24. mu.Unlock()
  25. time.Sleep(100 * time.Millisecond)
  26. continue
  27. }
  28. mu.Unlock()
  29. fmt.Printf("worker %d: start to work...\n", i)
  30. f(i)
  31. wg.Done()
  32. return
  33. }
  34. }(i + 1)
  35. }
  36. go func() {
  37. wg.Wait()
  38. c <- signal(struct{}{})
  39. }()
  40. return c
  41. }
  42. func main() {
  43. fmt.Println("start a group of workers...")
  44. mu := &sync.Mutex{}
  45. c := spawnGroup(worker, 5, mu)
  46. time.Sleep(5 * time.Second) // 模拟ready前的准备工作
  47. fmt.Println("the group of workers start to work...")
  48. mu.Lock()
  49. ready = true
  50. mu.Unlock()
  51. <-c
  52. fmt.Println("the group of workers work done!")
  53. }

我们用sync.Cond对上面的例子进行改造,改造后的代码如下:

  1. // go-sync-package-5.go
  2. package main
  3. import (
  4. "fmt"
  5. "sync"
  6. "time"
  7. )
  8. type signal struct{}
  9. var ready bool
  10. func worker(i int) {
  11. fmt.Printf("worker %d: is working...\n", i)
  12. time.Sleep(1 * time.Second)
  13. fmt.Printf("worker %d: works done\n", i)
  14. }
  15. func spawnGroup(f func(i int), num int, groupSignal *sync.Cond) <-chan signal {
  16. c := make(chan signal)
  17. var wg sync.WaitGroup
  18. for i := 0; i < num; i++ {
  19. wg.Add(1)
  20. go func(i int) {
  21. groupSignal.L.Lock()
  22. for !ready {
  23. groupSignal.Wait()
  24. }
  25. groupSignal.L.Unlock()
  26. fmt.Printf("worker %d: start to work...\n", i)
  27. f(i)
  28. wg.Done()
  29. }(i + 1)
  30. }
  31. go func() {
  32. wg.Wait()
  33. c <- signal(struct{}{})
  34. }()
  35. return c
  36. }
  37. func main() {
  38. fmt.Println("start a group of workers...")
  39. groupSignal := sync.NewCond(&sync.Mutex{})
  40. c := spawnGroup(worker, 5, groupSignal)
  41. time.Sleep(5 * time.Second) // 模拟ready前的准备工作
  42. fmt.Println("the group of workers start to work...")
  43. groupSignal.L.Lock()
  44. ready = true
  45. groupSignal.Broadcast()
  46. groupSignal.L.Unlock()
  47. <-c
  48. fmt.Println("the group of workers work done!")
  49. }

运行该实例:

  1. $go run go-sync-package-5.go
  2. start a group of workers...
  3. the group of workers start to work...
  4. worker 4: start to work...
  5. worker 4: is working...
  6. worker 1: start to work...
  7. worker 1: is working...
  8. worker 3: start to work...
  9. worker 3: is working...
  10. worker 5: start to work...
  11. worker 5: is working...
  12. worker 2: start to work...
  13. worker 2: is working...
  14. worker 1: works done
  15. worker 3: works done
  16. worker 4: works done
  17. worker 2: works done
  18. worker 5: works done
  19. the group of workers work done!

我们看到sync.Cond实例的初始化需要一个满足实现了sync.Locker接口的类型实例,通常我们使用sync.Mutex。条件变量需要这个互斥锁来同步临界区,保护用作条件的数据。各个等待条件成立的 goroutine 在加锁后判断条件是否成立,如果不成立,则调用sync.Cond的 Wait 方法进入等待状态。Wait 方法在 goroutine 挂起前会进行 Unlock 操作。

当 main goroutine 将ready置为 true 并调用sync.Cond的 Broadcast 方法后,各个阻塞的 goroutine 将被唤醒并从 Wait 方法中返回。
Wait 方法返回前,Wait 方法会再次加锁让 goroutine 进入临界区**。接下来 goroutine 会再次对条件数据进行判定,如果条件成立,则解锁并进入下一个工作阶段;如果条件依旧不成立,那么再次调用 Wait 方法挂起等待。

Broadcast() 是在 Unlock() 之前调用, 这表明在调用 Broadcast() 后, Wait() 内部是先解除阻塞并加锁.

5. 使用 sync.Once 实现单例(singleton)模式

在 Go 标准库中,我们看到sync.Once的“仅执行一次”语义被一些包用于初始化和资源清理的过程中,以避免重复执行初始化或资源关闭操作。比如:

  1. // $GOROOT/src/mime/type.go
  2. func TypeByExtension(ext string) string {
  3. once.Do(initMime)
  4. ... ...
  5. }
  6. // $GOROOT/src/io/pipe.go
  7. func (p *pipe) CloseRead(err error) error {
  8. if err == nil {
  9. err = ErrClosedPipe
  10. }
  11. p.rerr.Store(err)
  12. p.once.Do(func() { close(p.done) })
  13. return nil
  14. }

sync.Once的语义还是十分适合实现单例模式,并且实现起来十分简单。我们看下面的例子(注意:GetInstance 利用 sync.Once 实现的单例模式本可以十分简单,这里为了后续的说明,我们在例子中的单例函数实现中增加了很多不必要的代码):

  1. // go-sync-package-6.go
  2. package main
  3. import (
  4. "log"
  5. "sync"
  6. "time"
  7. )
  8. type Foo struct {
  9. }
  10. var once sync.Once
  11. var instance *Foo
  12. func GetInstance(id int) *Foo {
  13. defer func() {
  14. if e := recover(); e != nil {
  15. log.Printf("goroutine-%d: caught a panic: %s", id, e)
  16. }
  17. }()
  18. log.Printf("goroutine-%d: enter GetInstance\n", id)
  19. once.Do(func() {
  20. instance = &Foo{}
  21. time.Sleep(3 * time.Second)
  22. log.Printf("goroutine-%d: the addr of instance is %p\n", id, instance)
  23. panic("panic in once.Do function")
  24. })
  25. return instance
  26. }
  27. func main() {
  28. var wg sync.WaitGroup
  29. for i := 0; i < 5; i++ {
  30. wg.Add(1)
  31. go func(i int) {
  32. inst := GetInstance(i)
  33. log.Printf("goroutine-%d: the addr of instance returned is %p\n", i, inst)
  34. wg.Done()
  35. }(i + 1)
  36. }
  37. time.Sleep(5 * time.Second)
  38. inst := GetInstance(0)
  39. log.Printf("goroutine-0: the addr of instance returned is %p\n", inst)
  40. wg.Wait()
  41. log.Printf("all goroutines exit\n")
  42. }

运行该示例:

  1. $go run go-sync-package-6.go
  2. 2020/02/09 18:46:30 goroutine-1: enter GetInstance
  3. 2020/02/09 18:46:30 goroutine-4: enter GetInstance
  4. 2020/02/09 18:46:30 goroutine-5: enter GetInstance
  5. 2020/02/09 18:46:30 goroutine-3: enter GetInstance
  6. 2020/02/09 18:46:30 goroutine-2: enter GetInstance
  7. 2020/02/09 18:46:33 goroutine-1: the addr of instance is 0x1199b18
  8. 2020/02/09 18:46:33 goroutine-1: caught a panic: panic in once.Do function
  9. 2020/02/09 18:46:33 goroutine-1: the addr of instance returned is 0x0
  10. 2020/02/09 18:46:33 goroutine-4: the addr of instance returned is 0x1199b18
  11. 2020/02/09 18:46:33 goroutine-5: the addr of instance returned is 0x1199b18
  12. 2020/02/09 18:46:33 goroutine-3: the addr of instance returned is 0x1199b18
  13. 2020/02/09 18:46:33 goroutine-2: the addr of instance returned is 0x1199b18
  14. 2020/02/09 18:46:35 goroutine-0: enter GetInstance
  15. 2020/02/09 18:46:35 goroutine-0: the addr of instance returned is 0x1199b18
  16. 2020/02/09 18:46:35 all goroutines exit

通过上述例子,我们观察到:

  • once.Do 会等待 f 执行完毕后才返回,这期间其它执行 once.Do 函数的 goroutine(如上面运行结果中的 goroutine 2~5)将会阻塞等待
  • Do 函数返回后,后续的 goroutine 再执行 Do 函数将不再执行 f 并立即返回(如上面运行结果中的 goroutine-0);
  • 即便在函数 f 中出现 panic,sync.Once 原语也会认为 once.Do 执行完毕了,后续对 once.Do 的调用将不再执行 f。

6. 使用 sync.Pool 降低 GC 压力

sync.Pool 是一个数据对象缓存池,它具有如下特点:

  • 它是 goroutine 并发安全的,可以被多个 goroutine 同时使用;
  • 放入该缓存池中的数据对象的生命是暂时的,随时都可能被 GC 回收;
  • 缓存池中的数据对象是可以被重复利用的,这样可以一定程度上降低 GC 频繁回收数据对象占用内存的压力。

我们来看一个使用 sync.Pool 分配数据对象与通过 new 等常规方法分配数据对象对比的例子:

  1. // go-sync-package-7_test.go
  2. package main
  3. import (
  4. "bytes"
  5. "sync"
  6. "testing"
  7. )
  8. var bufPool = sync.Pool{
  9. New: func() interface{} {
  10. return new(bytes.Buffer)
  11. },
  12. }
  13. func writeBufFromPool(data string) {
  14. b := bufPool.Get().(*bytes.Buffer)
  15. b.Reset()
  16. b.WriteString(data)
  17. bufPool.Put(b)
  18. }
  19. func writeBufFromNew(data string) *bytes.Buffer {
  20. b := new(bytes.Buffer)
  21. b.WriteString(data)
  22. return b
  23. }
  24. func BenchmarkWithoutPool(b *testing.B) {
  25. b.ReportAllocs()
  26. for i := 0; i < b.N; i++ {
  27. writeBufFromNew("hello")
  28. }
  29. }
  30. func BenchmarkWithPool(b *testing.B) {
  31. b.ReportAllocs()
  32. for i := 0; i < b.N; i++ {
  33. writeBufFromPool("hello")
  34. }
  35. }

运行这个测试用例:

  1. $go test -bench . go-sync-package-7_test.go
  2. goos: darwin
  3. goarch: amd64
  4. BenchmarkWithoutPool-8 33605625 32.8 ns/op 64 B/op 1 allocs/op
  5. BenchmarkWithPool-8 53222953 22.8 ns/op 0 B/op 0 allocs/op
  6. PASS
  7. ok command-line-arguments 2.385s

我们看到通过sync.Pool来复用数据对象的方式可以有效降低内存分配频率,降低 GC 回收压力,从而提高处理性能。sync.Pool的一个典型应用就是建立像bytes.Buffer这样类型的临时缓存对象池:

  1. var bufPool = sync.Pool{
  2. New: func() interface{} {
  3. return new(bytes.Buffer)
  4. },
  5. }

但实践告诉我们这么用很可能会产生一些问题。由于 sync.Pool 的 Get 方法从缓存池中挑选 bytes.Buffer 数据对象时并未考虑该数据对象是否满足调用者的需求,因此一旦返回的 Buffer 对象是刚刚被“大数据”撑大后的,并且即将被长期用于处理一些“小数据”时,这个 Buffer 对象所占用的“大内存”将长时间得不到释放。一旦这类情况集中出现,将会给 Go 应用带来沉重的内存消耗负担。为此,目前的 Go 标准库采用两种方式来缓解这一问题。

  • 对要放回到缓存池中的数据对象大小做限制

在 Go 标准库 fmt 包中的代码中,我们看到:

  1. // $GOROOT/src/fmt/print.go
  2. func (p *pp) free() {
  3. // 正确使用sync.Pool要求每个条目需要具有大致相同的内存成本。
  4. // 若缓存池中存储的类型具有可变大小的缓冲区时,
  5. // 我们对放回缓存池的对象增加了一个最大缓冲区的硬限制(不能大于65536字节)
  6. //
  7. // See https://golang.org/issue/23199
  8. if cap(p.buf) > 64<<10 {
  9. return
  10. }
  11. p.buf = p.buf[:0]
  12. p.arg = nil
  13. p.value = reflect.Value{}
  14. p.wrappedErr = nil
  15. ppFree.Put(p)
  16. }

fmt 包对于要 put 回缓存池的 buffer 对象做了一个限制性校验:如果 buffer 的容量大于64<<10,则不让其回到缓存池中,这样可以一定程度上缓解处理小对象时重复利用大 Buffer 导致的内存占用问题

  • 建立多级缓存池

标准库的 http 包在处理 http2 数据时,预先建立了多个不同大小的缓存池:

  1. // $GOROOT/src/net/http/h2_bundle.go
  2. var (
  3. http2dataChunkSizeClasses = []int{
  4. 1 << 10,
  5. 2 << 10,
  6. 4 << 10,
  7. 8 << 10,
  8. 16 << 10,
  9. }
  10. http2dataChunkPools = [...]sync.Pool{
  11. {New: func() interface{} { return make([]byte, 1<<10) }},
  12. {New: func() interface{} { return make([]byte, 2<<10) }},
  13. {New: func() interface{} { return make([]byte, 4<<10) }},
  14. {New: func() interface{} { return make([]byte, 8<<10) }},
  15. {New: func() interface{} { return make([]byte, 16<<10) }},
  16. }
  17. )
  18. func http2getDataBufferChunk(size int64) []byte {
  19. i := 0
  20. for ; i < len(http2dataChunkSizeClasses)-1; i++ {
  21. if size <= int64(http2dataChunkSizeClasses[i]) {
  22. break
  23. }
  24. }
  25. return http2dataChunkPools[i].Get().([]byte)
  26. }
  27. func http2putDataBufferChunk(p []byte) {
  28. for i, n := range http2dataChunkSizeClasses {
  29. if len(p) == n {
  30. http2dataChunkPools[i].Put(p)
  31. return
  32. }
  33. }
  34. panic(fmt.Sprintf("unexpected buffer len=%v", len(p)))
  35. }