Golang 高效實踐之併發實踐context篇

我是碼客發表於2019-07-19

前言

在上篇Golang高效實踐之併發實踐channel篇中我給大家介紹了Golang併發模型,詳細的介紹了channel的用法,和用select管理channel。比如說我們可以用channel來控制幾個goroutine的同步和退出時機,但是我們需要close channel通知其他接受者,當通知和通訊的內容混在一起時往往比較複雜,需要把握好channel的讀寫時機,以及不能往已經關閉的channel中再寫入資料。如果有沒有一種更好的上下文控制機制呢?答案就是文章今天要介紹的context,context正在close channel的一種封裝,通常用來控制上下文的同步。

Context介紹

Context包定義了Context型別,Context型別攜帶著deadline生命週期,和取消訊號,並且可以攜帶使用者自定義的引數值。通常用Context來控制上下文,Context通過引數一層層傳遞,或者傳遞context的派生,一旦Context被取消,所有由該Context派生的Context也會取消。WithCancel,WithDeadline,和WithTimeout函式可以從一個Context中派生另外一個Context和一個cancel函式。呼叫cancel函式可以取消由context派生出來的Context。cancel函式會釋放context擁有的資源,所以當context不用時要儘快呼叫cancel。

Context應該作為函式的第一個引數,通常使用ctx命名,例如:

func DoSomething(ctx context.Context, arg Arg) error {

// … use ctx …

}

不要傳遞nil context,即便接受的函式允許我們這樣做也不要傳遞nil context。如果你不確定用哪個context的話可以傳遞context.TODO。

同一個context可以在不同的goroutine中訪問,context是執行緒安全的。

Context結構定義

type Context interface {
        // Deadline returns the time when work done on behalf of this context
        // should be canceled. Deadline returns ok==false when no deadline is
        // set. Successive calls to Deadline return the same results.
        Deadline() (deadline time.Time, ok bool)

        // Done returns a channel that's closed when work done on behalf of this
        // context should be canceled. Done may return nil if this context can
        // never be canceled. Successive calls to Done return the same value.
        //
        // WithCancel arranges for Done to be closed when cancel is called;
        // WithDeadline arranges for Done to be closed when the deadline
        // expires; WithTimeout arranges for Done to be closed when the timeout
        // elapses.
        //
        // Done is provided for use in select statements:
        //
        //  // Stream generates values with DoSomething and sends them to out
        //  // until DoSomething returns an error or ctx.Done is closed.
        //  func Stream(ctx context.Context, out chan<- Value) error {
        //      for {
        //          v, err := DoSomething(ctx)
        //          if err != nil {
        //              return err
        //          }
        //          select {
        //          case <-ctx.Done():
        //              return ctx.Err()
        //          case out <- v:
        //          }
        //      }
        //  }
        //
        // See https://blog.golang.org/pipelines for more examples of how to use
        // a Done channel for cancelation.
        Done() <-chan struct{}

        // If Done is not yet closed, Err returns nil.
        // If Done is closed, Err returns a non-nil error explaining why:
        // Canceled if the context was canceled
        // or DeadlineExceeded if the context's deadline passed.
        // After Err returns a non-nil error, successive calls to Err return the same error.
        Err() error

        // Value returns the value associated with this context for key, or nil
        // if no value is associated with key. Successive calls to Value with
        // the same key returns the same result.
        //
        // Use context values only for request-scoped data that transits
        // processes and API boundaries, not for passing optional parameters to
        // functions.
        //
        // A key identifies a specific value in a Context. Functions that wish
        // to store values in Context typically allocate a key in a global
        // variable then use that key as the argument to context.WithValue and
        // Context.Value. A key can be any type that supports equality;
        // packages should define keys as an unexported type to avoid
        // collisions.
        //
        // Packages that define a Context key should provide type-safe accessors
        // for the values stored using that key:
        //
        //     // Package user defines a User type that's stored in Contexts.
        //     package user
        //
        //     import "context"
        //
        //     // User is the type of value stored in the Contexts.
        //     type User struct {...}
        //
        //     // key is an unexported type for keys defined in this package.
        //     // This prevents collisions with keys defined in other packages.
        //     type key int
        //
        //     // userKey is the key for user.User values in Contexts. It is
        //     // unexported; clients use user.NewContext and user.FromContext
        //     // instead of using this key directly.
        //     var userKey key
        //
        //     // NewContext returns a new Context that carries value u.
        //     func NewContext(ctx context.Context, u *User) context.Context {
        //         return context.WithValue(ctx, userKey, u)
        //     }
        //
        //     // FromContext returns the User value stored in ctx, if any.
        //     func FromContext(ctx context.Context) (*User, bool) {
        //         u, ok := ctx.Value(userKey).(*User)
        //         return u, ok
        //     }
        Value(key interface{}) interface{}
}

WithCancel函式

func WithCancel(parent Context) (ctx Context, cancel CancelFunc)

WithCancel函式返回parent的一份拷貝和一個新的Done channel。當concel 函式被呼叫的時候或者parent的Done channel被關閉時(cancel被呼叫),context的Done channel將會被關閉。取消context將會釋放context相關的資源,所以當context完成時程式碼應該儘快呼叫cancel方法。例如:

package main

import (
    "context"
    "fmt"
)

func main() {
    // gen generates integers in a separate goroutine and
    // sends them to the returned channel.
    // The callers of gen need to cancel the context once
    // they are done consuming generated integers not to leak
    // the internal goroutine started by gen.
    gen := func(ctx context.Context) <-chan int {
        dst := make(chan int)
        n := 1
        go func() {
            for {
                select {
                case <-ctx.Done():
                    return // returning not to leak the goroutine
                case dst <- n:
                    n++
                }
            }
        }()
        return dst
    }

    ctx, cancel := context.WithCancel(context.Background())
    defer cancel() // cancel when we are finished consuming integers

    for n := range gen(ctx) {
        fmt.Println(n)
        if n == 5 {
            break
        }
    }
}

WithDeadline函式

func WithDeadline(parent Context, d time.Time) (Context, CancelFunc)

WithDeadline函式返回parent context調整deadline之後的拷貝,如果parent的deadline比要調整的d更早,那麼派生出來的context的deadline就等於parent的deadline。當deadline過期或者cancel函式被呼叫時,又或者parent的cancel函式被呼叫時,context的Done channel將會被觸發。例如:

package main

import (
    "context"
    "fmt"
    "time"
)

func main() {
    d := time.Now().Add(50 * time.Millisecond)
    ctx, cancel := context.WithDeadline(context.Background(), d)

    // Even though ctx will be expired, it is good practice to call its
    // cancelation function in any case. Failure to do so may keep the
    // context and its parent alive longer than necessary.
    defer cancel()

    select {
    case <-time.After(1 * time.Second):
        fmt.Println("overslept")
    case <-ctx.Done():
        fmt.Println(ctx.Err())
    }

}

Err方法會返回context退出的原因,這裡是context deadline exceeded。

WithTimeout函式

func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc)

WithTimeout相當於呼叫WithDeadline(parent, time.Now().Add(timeout)),例如:

package main

import (
    "context"
    "fmt"
    "time"
)

func main() {
    // Pass a context with a timeout to tell a blocking function that it
    // should abandon its work after the timeout elapses.
    ctx, cancel := context.WithTimeout(context.Background(), 50*time.Millisecond)
    defer cancel()

    select {
    case <-time.After(1 * time.Second):
        fmt.Println("overslept")
    case <-ctx.Done():
        fmt.Println(ctx.Err()) // prints "context deadline exceeded"
    }

}

Background函式

func Background() Context

Backgroud函式返回一個非nil的空context。該context不會cancel,沒有值,沒有deadline。通常在main函式中呼叫,初始化或者測試,作為頂級的context。

WithValue函式

func WithValue(parent Context, key, val interface{}) Context

WithValue函式返回parent的拷貝,並且key對應的值是value。例如:

package main

import (
    "context"
    "fmt"
)

func main() {
    type favContextKey string

    f := func(ctx context.Context, k favContextKey) {
        if v := ctx.Value(k); v != nil {
            fmt.Println("found value:", v)
            return
        }
        fmt.Println("key not found:", k)
    }

    k := favContextKey("language")
    ctx := context.WithValue(context.Background(), k, "Go")

    f(ctx, k)
    f(ctx, favContextKey("color"))

}

webserver實戰

有了上面的理論知識後,我將給大家講解一個webserver的編碼,其中就用到context的超時特性,以及上下文同步等。程式碼放在github上面,是從google search倉庫中fork出來並做了一些改動。該專案的程式碼用到go module來組織程式碼,如果對go module不熟悉的同學可以參考我的這篇部落格

server.go檔案是main包,裡面包含一個http server:

func main() {
    http.HandleFunc("/search", handleSearch)
    log.Fatal(http.ListenAndServe(":8080", nil))
}

例如通過/search?q=golang&timeout=1s訪問8080埠將會呼叫handle函式handleSearch來處理,handleSearch會解析出來要查詢的關鍵字golang,並且指定的超時時間是1s。該timeout引數會用於生成帶有timeout屬性的context,該context會貫穿整個請求的上下文,當超時時間觸發時會終止search。

func handleSearch(w http.ResponseWriter, req *http.Request) {
    // ctx is the Context for this handler. Calling cancel closes the
    // ctx.Done channel, which is the cancellation signal for requests
    // started by this handler.
    var (
        ctx    context.Context
        cancel context.CancelFunc
    )
    timeout, err := time.ParseDuration(req.FormValue("timeout"))
    if err == nil {
        // The request has a timeout, so create a context that is
        // canceled automatically when the timeout expires.
        ctx, cancel = context.WithTimeout(context.Background(), timeout)
    } else {
        ctx, cancel = context.WithCancel(context.Background())
    }
    defer cancel() // Cancel ctx as soon as handleSearch returns.

並且使用WithValue函式傳遞客戶端的IP:

const userIPKey key = 0

// NewContext returns a new Context carrying userIP.
func NewContext(ctx context.Context, userIP net.IP) context.Context {
    return context.WithValue(ctx, userIPKey, userIP)
}

google包裡面的Search函式實際的動作是將請求的引數傳遞給https://developers.google.com/custom-search,並且帶上context的超時屬性,當context超時的時候將會直接返回,不會等待https://developers.google.com/custom-search的返回。實際效果:

超時情況:

非超時情況:

總結

文章介紹了Golang的context包,並且介紹了包裡面的主要函式和作用,最後通過一個練習專案示例了context的實際應用。

參考

https://blog.golang.org/context

https://golang.org/pkg/context/

 

相關文章