问题
当我们有一个后台运行的goroutines通过其内部的构造函数创建一个对象以后,我们希望这个对象即使在goroutines没有被及时关闭以后,还能及时被垃圾回收。这是不可能的因为后台运行goroutines会一直运行并且会指向这个对象上。
解决方法
我们将这个返回的对象封装一下,然后在这个对象上使用finalizer,从而达到关闭后台goroutines的目的。
举个栗子
假设我们现在有个Go的静态客户端go-statsd-client,它会创建一个BufferedSender如下:
func NewBufferedSender(addr string, flushInterval time.Duration, flushBytes int) (Sender, error) {
simpleSender, err := NewSimpleSender(addr)
if err != nil {
return nil, err
}
sender := &BufferedSender{
flushBytes: flushBytes,
flushInterval: flushInterval,
sender: simpleSender,
buffer: senderPool.Get(),
shutdown: make(chan chan error),
}
sender.Start()
return sender, nil
}
复制代码
Start
方法复制创建一个gorutinues来定期刷新BufferedSender。
func (s *BufferedSender) Start() {
// write lock to start running
s.runmx.Lock()
defer s.runmx.Unlock()
if s.running {
return
}
s.running = true
s.bufs = make(chan *bytes.Buffer, 32)
go s.run()
}
复制代码
我们现在创建并且使用这个BufferedSender看看会发生什么
func Process() {
x := statsd.NewBufferedSender("localhost:2125", time.Second, 1024)
x.Inc("stat", 1, .1)
}
复制代码
最开始main gorotinues是指向x,但当我们退出Process
的时候BufferedSender仍然在运行,因为Start
所启动的goruntinues没有停止。
我们相当于泄漏了BufferedSender的内存因为我们忘记调用Close来关闭它了。
解决方案
参考一下Go的缓存库go-cache。你会注意到Cache
其实只是一个封装。
type Cache struct {
*cache
// If this is confusing, see the comment at the bottom of New()
}
type cache struct {
defaultExpiration time.Duration
items map[string]Item
mu sync.RWMutex
onEvicted func(string, interface{})
janitor *janitor
}
复制代码
当你new一个Cache对象的时候,他将返回一个代理者,代理者指向被封装的对象cache
,而不是返回的对象Cache
func New(defaultExpiration, cleanupInterval time.Duration) *Cache {
items := make(map[string]Item)
return newCacheWithJanitor(defaultExpiration, cleanupInterval, items)
}
func newCacheWithJanitor(de time.Duration, ci time.Duration, m map[string]Item) *Cache {
c := newCache(de, m)
// This trick ensures that the janitor goroutine (which--granted it
// was enabled--is running DeleteExpired on c forever) does not keep
// the returned C object from being garbage collected. When it is
// garbage collected, the finalizer stops the janitor goroutine, after
// which c can be collected.
C := &Cache{c}
if ci > 0 {
runJanitor(c, ci)
runtime.SetFinalizer(C, stopJanitor)
}
return C
}
func runJanitor(c *cache, ci time.Duration) {
j := &janitor{
Interval: ci,
stop: make(chan bool),
}
c.janitor = j
go j.Run(c)
}
复制代码
参考它把我们代码改成
func Process() {
x := cache.New(time.Second, time.Minute)
}
复制代码
非常重要的区别的是这里的Cache
是可以被垃圾回收的,即使cache
对象并不能被回收。我们将GC行为器SetFinalizer设置在cache
上。stopJanitor
函数会通知后台运行的goruntines停止运行。
runtime.SetFinalizer(C, stopJanitor)
...
...
func stopJanitor(c *Cache) {
c.janitor.stop <- true
}
复制代码
当后台gorutinues被停止以后,就没有东西再继续指向cache
了。
然后它就会被垃圾回收。
什么时候使用它
这其实是取决于用你这个库的用户是怎么想的,他们是否希望能明确地创建并能关闭后台进程。Go的http.Serve就是一个很好的例子。注意到这里不是func NewHTTPServer() *http.Server
,而是使用一个对象,并且用户可以在准备就绪时显式启动(或停止)服务器。
基于这个最佳实现,如果你确实想控制你的后台进程在什么时候被关闭的时候,你仍然应该暴露一个Close
函数允许用户关闭后台gorutines来达到回收内存的目的。但是如果你认为让用户自己去调用Close
比较麻烦,你就可以加一个finalizer的封装来确保内润以及你所创建的goruntinue能在最后被正确地回收不管有没有调用Close
有疑问加站长微信联系(非本文作者)