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

野生程序元 · · 539 次点击 · · 开始浏览    
这是一个创建于 的文章,其中的信息可能已经有所发展或是发生改变。

原文:medium.com/@cep21/gos-…

问题

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


有疑问加站长微信联系(非本文作者)

本文来自:掘金

感谢作者:野生程序元

查看原文:[译]Go使用封装返回模式回收被goroutines占用的内存

入群交流(和以上内容无关):加入Go大咖交流群,或添加微信:liuxiaoyan-s 备注:入群;或加QQ群:692541889

539 次点击  
加入收藏 微博
暂无回复
添加一条新回复 (您需要 登录 后才能回复 没有账号 ?)
  • 请尽量让自己的回复能够对别人有帮助
  • 支持 Markdown 格式, **粗体**、~~删除线~~、`单行代码`
  • 支持 @ 本站用户;支持表情(输入 : 提示),见 Emoji cheat sheet
  • 图片支持拖拽、截图粘贴等方式上传