10

错误处理与资源释放

 3 years ago
source link: https://www.zenlife.tk/error-handle.md
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

错误处理与资源释放

2017-03-20

在出错的情况下,如何处理好资源的释放,在Go语言里面相当考验使用者经验。如果能把错误处理做到位,基本上可以算玩得很溜了。资源主要是指goroutine,但这个话题并不特指。记得以前就说过,对于并发编程,我一直是心存敬畏之心的,即使有Go这么强大的语言。

这里先提出三个问题,并没有给出标准的答案。可以一起探讨。

  1. error的时候要不要调close
  2. close的时候又error了怎么办
  3. cancel的时候是否返回error

好,现在先放下这些问题不谈,来看一段代码。

func worker(done chan error) {
    err := doSomething()
    if err != nil {
        done <- err
    }
    done <- nil
}
func f() error {
    doneCh := make(chan error)
    for i:=0; i

这段代码的错误处理有什么问题?先不要看答案,思考几分钟...

...

......

假设 <-doneCh 返回error了,那么函数f直接返回,之后再也没人读doneCh里面的数据,于是worker的goroutine会永远阻塞在 done <- err —- 泄漏了!

这是非常一个典型的例子,worker的goroutine无法释放,那么它引用的内存就无法回收,程序就会内存泄漏。再回头看一个这篇文章的标题:错误处理与资源释放。在错误处理做的不到位的时候,很容易漏掉资源的释放。

这个地方就是遇到error直接退出导致的。有些简单的修改的办法,比如:

方案一,带buffer的channel,直接让channel大小跟worker数量一样大,就不会有done <- err的阻塞问题,总是放得进去的。

    doneCh := make(chan error, workerNum)

方案二,调用者即使遇到了错误,也等所有worker完成之后才返回。

    var err error
    for i:=0; i

这样写代码又容易遇到另一个问题,即error覆盖,保留第一个,还是保留最后一个?其它打log?到底怎么做比较好呢?可以想一想。

方案三,单独加一个closeCh,专门用于退出。

func worker(done chan error, closed chan struct{}) {
    err := doSomething()
    select {
    case done <- err:
    case <-closed:
    }
}
func f() error {
    doneCh := make(chan error)
    closed := make(chan struct{})
    for i:=0; i

close一个channel时,所有读这个channel的goroutine都可以收到通知,我以前说过这个技巧

代码看完了,思维再跳出来,把问题抽象一下。

我们有可能遇到这样的场景,有A/B/C一个序列的操作要完成,而每个操作都会占资源。如果我们处理到A的时候已经遇到错误了,是否应该回收B和C的资源?答案是肯定的啦,正确的错误处理,不应该发生资源泄漏。假如资源提供了一个叫Close()的操作,遇到错误的场景,我们是应该掉用Close()的。什么时候调用呢?

采用不同的约定方式,有两种处理close的时机:

第一种是,在发生error的位置,妥善处理好资源的释放。即处理完A遇到错误,不能直接return,要先close掉B和C的资源。上在方案二和方案三都是这样的例子。

另一种,把error处理跟资源释放独立起来。流程是这样的,先分配好ABC的资源,函数f()里面依次调用ABC,不管f()是否返回error,都是f返回后在外面释放资源。可以把方案三改一下作为例子:

    // 统一分配好资源
    closed := make(chan struct{})
    doneCh := make(chan error)
    for i:=0; i

close的时候又error了怎么办?这个问题太恶心了。发生error了想处理好资源释放再返回,然而释放资源也可能继续error,非常需要注意到error的覆盖。

方案一和方案二的代码其实有那么一点不完美。为什么呢?如果遇到错误,调用者对剩下的结果已经不关注了。对比一下,方案三里面,如果遇到第一个出错了,就会调close,直接取消掉后面的运算。而方案一和方案二,还会把运算做完,浪费资源。方案三某种意义上讲,其实就是支持了cancel操作。

Go的标准库里面有个context包,是可以提供cancel的。这是一个非常牛逼的feature。但是有个问题,cancel操作跟普通的错误处理的调用链是完全不同的。普通的错误处理的调用链是一级一级的,像一个链表,而cancel像一颗树。这个思考起来又是一个很头痛的问题。


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK