Golang之Context的使用


 

轉載自:http://www.nljb.net/default/Golang%E4%B9%8BContext%E7%9A%84%E4%BD%BF%E7%94%A8/

簡介

在golang中的創建一個新的線程並不會返回像c語言類似的pid
所有我們不能從外部殺死某個線程,所有我就得讓它自己結束
之前我們用channel+select的方式,來解決這個問題
但是有些場景實現起來比較麻煩,例如由一個請求衍生出多個線程
並且之間需要滿足一定的約束關系,以實現一些諸如:
有效期,中止線程樹,傳遞請求全局變量之類的功能。
於是google 就為我們提供一個解決方案,開源了context包。
使用context包來實現上下文功能 .....
約定:需要在你的方法的傳入參數的第一個參數是context.Context的變量。

其實本身非常簡單,在導入這個包之后,初始化Context對象,在每個資源訪問方法中都調用它,然后在使用時檢查Context對象是否已經被Cancel,如果是就釋放綁定的資源


源碼剖析

context.Context 接口

  • context包里的方法是線程安全的,可以被多個線程使用
  • 當Context被canceled或是timeout, Done返回一個被closed 的channel
  • 在Done的channel被closed后, Err代表被關閉的原因
  • 如果存在, Deadline 返回Context將要關閉的時間
  • 如果存在,Value 返回與 key 相關了的值,不存在返回 nil
// context 包的核心
type Context interface {               
    Done() <-chan struct{}      
    Err() error 
    Deadline() (deadline time.Time, ok bool)
    Value(key interface{}) interface{}
}

我們不需要手動實現這個接口,context 包已經給我們提供了兩個

一個是 Background(),一個是 TODO()
這兩個函數都會返回一個Context的實例
只是返回的這兩個實例都是空 Context。
/*
    TODO返回一個非空,空的上下文
    在目前還不清楚要使用的上下文或尚不可用時
*/
context.TODO()
/*
    Background返回一個非空,空的上下文。
    這是沒有取消,沒有值,並且沒有期限。
    它通常用於由主功能,初始化和測試,並作為輸入的頂層上下文
*/
context.Background()

主要方法

func WithCancel(parent Context) (ctx Context, cancel CancelFunc)
func WithDeadline(parent Context, deadline time.Time) (Context, CancelFunc)
func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc)
func WithValue(parent Context, key interface{}, val interface{}) Context

WithCancel 對應的是 cancelCtx ,其中,返回一個 cancelCtx ,同時返回一個 CancelFunc,CancelFunc 是 context 包中定義的一個函數類型:type CancelFunc func()。調用這個 CancelFunc 時,關閉對應的c.done,也就是讓他的后代goroutine退出

WithDeadline 和 WithTimeout 對應的是 timerCtx ,WithDeadline 和 WithTimeout 是相似的,WithDeadline 是設置具體的 deadline 時間,到達 deadline 的時候,后代 goroutine 退出,而 WithTimeout 簡單粗暴,直接 return WithDeadline(parent, time.Now().Add(timeout))

WithValue 對應 valueCtx ,WithValue 是在 Context 中設置一個 map,拿到這個 Context 以及它的后代的 goroutine 都可以拿到 map 里的值

context的創建

所有的context的父對象,也叫根對象,是一個空的context,它不能被取消,它沒有值,從不會被取消,也沒有超時時間,它常常作為處理request的頂層context存在,然后通過WithCancel、WithTimeout函數來創建子對象來獲得cancel、timeout的能力

當頂層的request請求函數結束后,我們就可以cancel掉某個context,從而通知別的routine結束

WithValue方法可以把鍵值對加入context中,讓不同的routine獲取

官方案例

// 在 handle 環境中使用 
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())
    }
    // ----------------
    // 這樣隨着cancel的執行,所有的線程都隨之結束了 ...
    go A(ctx) +1
    go B(ctx) +2
    go C(ctx) +3
    // ----------------
    defer cancel() // Cancel ctx as soon as handleSearch returns.
}

// 監聽 ctx.Done() 結束 ...
func A(ctx context.Context) int {
    // ... TODO
    select {
    case <-ctx.Done():
            return -1
    default:
        // 沒有結束 ... 執行 ...
    }
}

ctxhttp.go

package ctxhttp // import "golang.org/x/net/context/ctxhttp"

import (
    "io"
    "net/http"
    "net/url"
    "strings"

    "golang.org/x/net/context"
)

// Do sends an HTTP request with the provided http.Client and returns
// an HTTP response.
//
// If the client is nil, http.DefaultClient is used.
//
// The provided ctx must be non-nil. If it is canceled or times out,
// ctx.Err() will be returned.
func Do(ctx context.Context, client *http.Client, req *http.Request) (*http.Response, error) {
    if client == nil {
        client = http.DefaultClient
    }
    resp, err := client.Do(req.WithContext(ctx))
    // If we got an error, and the context has been canceled,
    // the context's error is probably more useful.
    if err != nil {
        select {
        case <-ctx.Done():
            err = ctx.Err()
        default:
        }
    }
    return resp, err
}

// Get issues a GET request via the Do function.
func Get(ctx context.Context, client *http.Client, url string) (*http.Response, error) {
    req, err := http.NewRequest("GET", url, nil)
    if err != nil {
        return nil, err
    }
    return Do(ctx, client, req)
}

// Head issues a HEAD request via the Do function.
func Head(ctx context.Context, client *http.Client, url string) (*http.Response, error) {
    req, err := http.NewRequest("HEAD", url, nil)
    if err != nil {
        return nil, err
    }
    return Do(ctx, client, req)
}

// Post issues a POST request via the Do function.
func Post(ctx context.Context, client *http.Client, url string, bodyType string, body io.Reader) (*http.Response, error) {
    req, err := http.NewRequest("POST", url, body)
    if err != nil {
        return nil, err
    }
    req.Header.Set("Content-Type", bodyType)
    return Do(ctx, client, req)
}

// PostForm issues a POST request via the Do function.
func PostForm(ctx context.Context, client *http.Client, url string, data url.Values) (*http.Response, error) {
    return Post(ctx, client, url, "application/x-www-form-urlencoded", strings.NewReader(data.Encode()))
}

使用示例

package main

import (
    "fmt"
    "time"

    "golang.org/x/net/context"
)

func Cdd(ctx context.Context) int {
    fmt.Println(ctx.Value("NLJB"))
    select {
    // 結束時候做點什么 ...
    case <-ctx.Done():
        return -3
    default:
        // 沒有結束 ... 執行 ...
    }
}

func Bdd(ctx context.Context) int {
    fmt.Println(ctx.Value("HELLO"))
    fmt.Println(ctx.Value("WROLD"))
    ctx = context.WithValue(ctx, "NLJB", "NULIJIABEI")
    go fmt.Println(Cdd(ctx))
    select {
    // 結束時候做點什么 ...
    case <-ctx.Done():
        return -2
    default:
        // 沒有結束 ... 執行 ...
    }
}

func Add(ctx context.Context) int {
    ctx = context.WithValue(ctx, "HELLO", "WROLD")
    ctx = context.WithValue(ctx, "WROLD", "HELLO")
    go fmt.Println(Bdd(ctx))
    select {
    // 結束時候做點什么 ...
    case <-ctx.Done():
        return -1
    default:
        // 沒有結束 ... 執行 ...
    }
}

func main() {

    // 自動取消(定時取消)
    {
        timeout := 3 * time.Second
        ctx, _ := context.WithTimeout(context.Background(), timeout)
        fmt.Println(Add(ctx))
    }
    // 手動取消
    //  {
    //      ctx, cancel := context.WithCancel(context.Background())
    //      go func() {
    //          time.Sleep(2 * time.Second)
    //          cancel() // 在調用處主動取消
    //      }()
    //      fmt.Println(Add(ctx))
    //  }
    select {}

}
package main

import (
    "fmt"
    "time"
    "golang.org/x/net/context"
)

// 模擬一個最小執行時間的阻塞函數
func inc(a int) int {
    res := a + 1                // 雖然我只做了一次簡單的 +1 的運算,
    time.Sleep(1 * time.Second) // 但是由於我的機器指令集中沒有這條指令,
    // 所以在我執行了 1000000000 條機器指令, 續了 1s 之后, 我才終於得到結果。B)
    return res
}

// 向外部提供的阻塞接口
// 計算 a + b, 注意 a, b 均不能為負
// 如果計算被中斷, 則返回 -1
func Add(ctx context.Context, a, b int) int {
    res := 0
    for i := 0; i < a; i++ {
        res = inc(res)
        select {
        case <-ctx.Done():
            return -1
        default:
        // 沒有結束 ... 執行 ...
        }
    }
    for i := 0; i < b; i++ {
        res = inc(res)
        select {
        case <-ctx.Done():
            return -1
        default:
        // 沒有結束 ... 執行 ...
        }
    }
    return res
}

func main() {
    {
        // 使用開放的 API 計算 a+b
        a := 1
        b := 2
        timeout := 2 * time.Second
        ctx, _ := context.WithTimeout(context.Background(), timeout)
        res := Add(ctx, 1, 2)
        fmt.Printf("Compute: %d+%d, result: %d\n", a, b, res)
    }
    {
        // 手動取消
        a := 1
        b := 2
        ctx, cancel := context.WithCancel(context.Background())
        go func() {
            time.Sleep(2 * time.Second)
            cancel() // 在調用處主動取消
        }()
        res := Add(ctx, 1, 2)
        fmt.Printf("Compute: %d+%d, result: %d\n", a, b, res)
    }
}


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM