1. 程式人生 > >GOLANG的context和併發模型

GOLANG的context和併發模型

GOLANG1.7新增了context,最初這個package是在golang.org/x/net/context中的,後來證實對很多程式都是必須的,就納入了標準庫。

對context的介紹是在context,讀這個blog之前,要先讀pipelines(pipelines提出了使用close(chan)的方式廣播退出事件)。

一般來說,context是用在request的處理,例如http請求的處理中,可能會開啟多個goroutine,比如:

http.HandleFunc(func(w http.ResponseWriter, r *http.Request){
    wg := sync.WaitGroup{}
    wg.Add(2
) var result0 string go func(){ defer wg.Done() res0,err := http.Get("http://server0/api0") // Parse res0 to result0. }() var result1 string go func(){ defer wg.Done() res1,err := http.Get("http://server1/api1") // Parse res1 to result1. }() wg.Wait() w.Write([]byte
(result0 + result1) })

實際上這個程式是不能這麼寫的,如果這兩個goroutine請求的時間比較長,讓使用者一直等著麼?如果使用者取消了請求,關閉了連線呢?如果使用者指定了超時時間呢?

另外,考慮如何關閉一個http伺服器,比如需要關閉listener後重新偵聽一個新的埠:

    var server *http.Server

    go func() {
        server = &http.Server{Addr: addr, Handler: nil}
        if err = server.ListenAndServe(); err
!= nil { ol.E(nil, "API serve failed, err is", err) return } }()

我們如何確保goroutine已經退出後,然後才返回重新開啟伺服器?如果只是server.Close(或者GO1.8之前用listener.Close),如何接收外部的事件?如果是goroutine自己的問題,例如端口占用了,如何通知程式退出?直接用os.Exit明顯是太粗魯的做法。

context中,有一段話非常關鍵:

A Context does not have a Cancel method for the same reason the 
Done channel is receive-only: the function receiving a 
cancelation signal is usually not the one that sends the signal. 
In particular, when a parent operation starts goroutines for sub-
operations, those sub-operations should not be able to cancel the 
parent. Instead, the WithCancel function (described below) 
provides a way to cancel a new Context value.

也就是說,context沒有提供Cancel方法,因為parent goroutine會呼叫Cancel,在所有sub goroutines中只需要讀context.Done()就可以,也就是隻是接收退出訊號。

還有一處地方,說明了Context應該放在引數中:

Server frameworks that want to build on Context should provide 
implementations of Context to bridge between their packages and 
those that expect a Context parameter. Their client libraries 
would then accept a Context from the calling code. By 
establishing a common interface for request-scoped data and 
cancelation, Context makes it easier for package developers to 
share code for creating scalable services.

只讀取chan的好處是,可以使用close(chan)方式通知所有goroutine退出。

使用context和WaitGroup,同步和取消伺服器的例子:

func HTTPAPIServe(ctx context.Context) {
    http.HandleFunc("/api", func(w http.ResponseWriter, r *http.Request) {
    })

    ctx,cancel := context.WithCancel(ctx)

    wg := &sync.WaitGroup{}
    defer wg.Wait()

    wg.Add(1)
    var server *http.Server
    go func(ctx context.Context) {
        defer wg.Done()
        defer cancel()
        server = &http.Server{Addr: addr, Handler: nil}
        _ = server.ListenAndServe()
    }(ctx)

    select {
    case <-ctx.Done():
        server.Close()
    }
}

wg := sync.WaitGroup{}
defer wg.Wait()

ctx, cancel := context.WithCancel(context.Background())

wg.Add(1)
go func(ctx context.Context) {
    defer wg.Done()
    defer cancel()
    HTTPAPIServe(ctx)
}(ctx)

wg.Add(1)
go func(ctx context.Context) {
    defer wg.Done()
    defer cancel()
    // Other server, such as:
    // UDPServer(ctx)
}(ctx)

這個程式實際上包含了幾條通道:

  1. 如果需要主動退出,通知所有listener做清理然後退出,可以在parent goroutine呼叫cancel。上面是在任意goroutine退出後,通知所有goroutine退出。
  2. 如果某個sub-goroutine需要通知其他sub-goroutine退出,不應該直接呼叫cancel方法,而是通過chan(上面是quit)在goroutine返回時告知parent goroutine,由parent goroutine處理。
  3. 在sub-goroutine中,如果還需要啟動goroutine,可以新開context做同步。如果是可以直接用select,那就可以不用新開goroutine,而是直接select ctx,等待退出。一般而言,goroutine的退出,就意味著一個退出的事件,而這個事件應該由parent goroutine處理,而不能直接廣播給其他goroutine。

特別是對於library,在引數中支援context,是非常重要的一個要素,這樣可以收取到user要求退出或cancel的訊號。