Discover-sync.Pool

其实很久之前就用到了这个东西,起因是collecter程序占用太多内存了,然后就用sync.Pool复用额外消耗的一次性内存,避免GC周期太长使内存来不及释放而导致的OOM。

https://golang.org/pkg/sync/#Pool

简单的说,就是在一个池子里放了一些“东西”,这些东西是某种特殊的类型,用的时候需要指定。

池子会随着你的取用而扩张,比如说,池子里面放了扳手(扳手池),现在有10个工人依次取用,当第一个人取的时候,“扳手池”发现没有扳手,ok,new一个出来; 当第一个工人用完了的时候,把扳手放回扳手池,然后第二个人取的时候,扳手池就直接返回那个扳手就可以了。嗯……如果第一个工人没有归还呢,那么扳手池就要重新new一个扳手了,也就是这种情况:10个工人同时取用扳手,那么扳手池就得new10个新的出来了。

一个测试:

https://gist.github.com/wrfly/7de7f1e0c87860aa2f92dc6ed64cb75b

Makefile:

.PHONY: build run test
NAME := $(shell basename `pwd`)

build:
	go build

run:
	./$(NAME)

test: build run
	go tool pprof -lines $(NAME) mem.porf

上面那个gist中,有几个测试,在这种情况下:

	wg.Add(alloc)
	for i := 0; i < alloc; i++ {
		go func(num int) {
			// justMake()
			// bufPoolGet()
			bufPoolGetAndPut(num)
			// bufPoolSleepAndGetAndPut(num)
			wg.Done()
		}(i)
	}

也就是拿了接着放回去的时候,结果如下:

➜  syncPool_mem_usage_test make run
go build
./syncPool_mem_usage_test
newmake: 4
reused: 9996
reused slice(maybe equal to newmake) len: 0
➜  syncPool_mem_usage_test 

也就是说,新分配了4个1e6长度的[]byte,其余的都是复用的。

但是这里有要注意的是,如果将 buf 中的内容打印出来(上面的num),reuse的效率就会下降几个数量级了,我还试过用append的方式将复用的内容追加到某个地方,复用的buf也会减少很多很多,分析原因,认为不论是print还是append都会占用时间,影响了Put的效率,从而Get的时候就Get慢了,就new了新的出来。

嗯,其实上面是为了测试Pool里面到底存了多少东西,是把所有put进去的都存了,还是只存了最后put进去的。

该程序最后加了这么一段:

	for i := 0; i < alloc; i++ {
		b := bufPool.Get()
		buf := b.([]byte)
		if buf[0] != 0 {
			fmt.Printf("reuse %s", buf)
		}
	}

发现结果跟理论有很大出入,按理说应该打印make的那几个,但有时候回打印,有时候就没有。

➜  syncPool_mem_usage_test make run
go build
./syncPool_mem_usage_test
reuse [2]
reuse [175]
reuse [174]
reuse [210]
newmake: 13
reused: 9987
reused slice(maybe equal to newmake) len: 4
➜  syncPool_mem_usage_test 

应该是被整没了:

// Any item stored in the Pool may be removed automatically at any time without
// notification. If the Pool holds the only reference when this happens, the
// item might be deallocated.
//

所以放里面的东西并不保险,内存是会省的,但东西会丢。

所以说,池子里面的东西,会丢,比如new了10个扳手,再用的时候里面也许就只有8个了,有两个被销毁了(be removed automatically at any time without notification)(也是够狠的),然后就找不到他们了,想再用的话,就只能new新的。

gist中还有不同的测试,最主要的是对内存消耗的测试。其实不用测也知道,GetAndPut是最省内存的,上面只是验证理论,毕竟不能别人说啥就是啥,是吧。

感兴趣的同学可以把代码和Makefile都跑一跑,在我这里:

  • justMake用了148.59MB
  • bufPoolGet用了27.02MB
  • bufPoolGetAndPut用了1.13MB
  • bufPoolSleepAndGetAndPut用了332.07MB|282.54MB|16137.43kB|56.28MB 总之结果差异很大…… 可能是时间的原因,没验证(但这个结果的确很蹊跷,值得深究)

消耗不了多少内存。

结论

在高并发的情况下,使用sync.Pool能够节省内存.

comments powered by Disqus