初識go的tomb包

python修行路發表於2018-09-20

在分析github.com/hpcloud/tail 這個包的原始碼的時候,發現這個包裡用於了一個另外一個包,自己也沒有用過,但是這個包在tail這個包裡又起來非常大的作用

當時並沒有完全弄明白這個包的用法和作用,所以又花時間找了這個包的使用和相關文件,其中看了https://blog.labix.org/2011/10/09/death-of-goroutines-under-control 這篇文章整理的挺好的,自己對這個文章進行了簡單的翻譯,下面這個文章中的使用是gopkg.in/tomb.v2

Death of goroutines under control

很多人被go語言吸引的原因是其非常好的併發性,以及channel, 輕量級執行緒(goroutine)等這些特性

並且你也會經常在社群或者其他文章裡看到這樣一句話:

Do not communicate by sharing memory;

instead, share memory by communicating.

這個模型非常合理,以這種方式處理問題,在設計演算法時會產生顯著差異,但這個並不是什麼新聞

What I address in this post is an open aspect we have today in Go related to this design: the termination of background activity.(不知道怎麼翻譯了)

作為一個例子,我們構建一個簡單的goroutine,通過一個channel 傳送

 

type LineReader struct {
        Ch chan string
        r  *bufio.Reader
}

func NewLineReader(r io.Reader) *LineReader {
        lr := &LineReader{
                Ch: make(chan string),
                r:  bufio.NewReader(r),
        }
        go lr.loop()
        return lr
}

The type has a channel where the client can consume lines from, and an internal buffer
used to produce the lines efficiently. Then, we have a function that creates an initialized
reader, fires the reading loop, and returns. Nothing surprising there.

接著看看loop方法

func (lr *LineReader) loop() {
        for {
                line, err := lr.r.ReadSlice(`
`)
                if err != nil {
                        close(lr.Ch)
                        return
                }
                lr.Ch <- string(line)
        }
}

在這個loop中,我們將從從緩衝器中獲取每行的內容,如果出現錯誤時關閉通道並停止,否則則將讀取的一行內容放到channel中

也許channel接受的那一方忙於其他處理,而導致其會阻塞。 這個簡單的例子對於很多go開發者應該非常熟悉了

 

但這裡有兩個與終止邏輯相關的細節:首先錯誤資訊被丟棄,然後無法通過一種更加乾淨的方式從外部終端程式

當然,錯誤很容易記錄下來,但是如果我們想要將它儲存資料庫中,或者通過網路傳送它,或者甚至考慮到它的性質,在很過情況下

乾淨的停止也是非常有價值的

這並非是一個非常難以做到的事情,但是今天沒有簡單一致的方法來處理,或者也許沒有,而go中的Tom包就是試圖解決這個問題的

 

這個模型很簡單:tomb跟蹤一個或者多個goroutines是活著的,還是已經死了,以及死亡的原因

為了理解這個模型,我們把上面的LineReader例子進行改寫:

type LineReader struct {
        Ch chan string
        r  *bufio.Reader
        t  tomb.Tomb
}

func NewLineReader(r io.Reader) *LineReader {
        lr := &LineReader{
                Ch: make(chan string),
                r:  bufio.NewReader(r),
        }
        lr.t.Go(lr.loop)
        return lr
}

這裡有一些有趣的點:

首先,現在出現的錯誤結果與任何可能失敗的go函式或者方法一樣。

hen, the previously loose error is now returned, 標記這個goroutine終止的原因,

最後這個通道的傳送被調增,以便於不管goroutine因為上面原因死亡都不會阻塞

A Tomb has both Dying and Dead channels returned by the respective methods, which are closed when the Tomb state changes accordingly. These channels enable explicit blocking until the state changes, and also to selectively unblock select statements in those cases, as done above.

 

通過上面的說的,我們可以簡單的引入Stop方法從外部同步請求清楚goroutine

func (lr *LineReader) Stop() error {
        lr.t.Kill(nil)
        return lr.t.Wait()
}

在這種情況下,Kill方法會將正在執行的goroutine從外部將其置於一個死亡狀態,並且Wait將阻塞直到goroutine通過

自己終止返回。 即使由於內部錯誤,groutine已經死亡或者處於死亡狀態,此過程也會正常執行,因為只有第一次用一個實際的錯誤呼叫Kill被記錄為goroutine死亡原因。 

The nil value provided to t.Kill is used as a reason when terminating cleanly without an actual error, and it causes Wait to return nil once the goroutine terminates, flagging a clean stop per common Go idioms.

 

關於gopkg.in/tomb.v2的官網說明的一段話:

The tomb package handles clean goroutine tracking and termination.

The zero value of a Tomb is ready to handle the creation of a tracked goroutine via its Go method, and then any tracked goroutine may call the Go method again to create additional tracked goroutines at any point.

If any of the tracked goroutines returns a non-nil error, or the Kill or Killf method is called by any goroutine in the system (tracked or not), the tomb Err is set, Alive is set to false, and the Dying channel is closed to flag that all tracked goroutines are supposed to willingly terminate as soon as possible.

Once all tracked goroutines terminate, the Dead channel is closed, and Wait unblocks and returns the first non-nil error presented to the tomb via a result or an explicit Kill or Killf method call, or nil if there were no errors.

It is okay to create further goroutines via the Go method while the tomb is in a dying state. The final dead state is only reached once all tracked goroutines terminate, at which point calling the Go method again will cause a runtime panic.

Tracked functions and methods that are still running while the tomb is in dying state may choose to return ErrDying as their error value. This preserves the well established non-nil error convention, but is understood by the tomb as a clean termination. The Err and Wait methods will still return nil if all observed errors were either nil or ErrDying.

 

關於gopkg.in/tomb.v1使用例子

在golang官網上看到了這樣一個例子,覺得用的挺好的就放這裡

package main

import (
    "gopkg.in/tomb.v1"
    "log"
    "sync"
    "time"
)

type foo struct {
    tomb tomb.Tomb
    wg   sync.WaitGroup
}

func (f *foo) task(id int) {
    for i := 0; i < 10; i++ {
        select {
        case <-time.After(1e9):
            log.Printf("task %d tick
", id)
        case <-f.tomb.Dying():
            log.Printf("task %d stopping
", id)
            f.wg.Done()
            return
        }
    }
}

func (f *foo) Run() {
    f.wg.Add(10)
    for i := 0; i < 10; i++ {
        go f.task(i)
    }
    go func() {
        f.wg.Wait()
        f.tomb.Done()
    }()
}

func (f *foo) Stop() error {
    f.tomb.Kill(nil)
    return f.tomb.Wait()
}

func main() {
    var f foo
    f.Run()
    time.Sleep(3.5e9)
    log.Printf("calling stop
")
    f.Stop()
    log.Printf("all done
")
}

在關於tomb這個包的說明上,說的也非常清楚,tomb包用於追蹤一個goroutine的宣告週期,如:as alive,dying or dead and the reason for its death

關於v1 版本官網的說明

The tomb package offers a conventional API for clean goroutine termination.

A Tomb tracks the lifecycle of a goroutine as alive, dying or dead, and the reason for its death.

The zero value of a Tomb assumes that a goroutine is about to be created or already alive. Once Kill or Killf is called with an argument that informs the reason for death, the goroutine is in a dying state and is expected to terminate soon. Right before the goroutine function or method returns, Done must be called to inform that the goroutine is indeed dead and about to stop running.

A Tomb exposes Dying and Dead channels. These channels are closed when the Tomb state changes in the respective way. They enable explicit blocking until the state changes, and also to selectively unblock select statements accordingly.

When the tomb state changes to dying and there`s still logic going on within the goroutine, nested functions and methods may choose to return ErrDying as their error value, as this error won`t alter the tomb state if provided to the Kill method. This is a convenient way to follow standard Go practices in the context of a dying tomb..

 

小結

可以從上面的文章以及使用例子上看出,tomb包是一個非常實用的一個包,後面會繼續整理一下關於tomb v1版本的原始碼,看看人家是如何實現的,學習學習