[译]Go使用封装返回模式回收被goroutines占用的内存

当我们有一个后台运行的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