GoLang并發(fā)控制(下)

context的字面意思是上下文,是一個(gè)比較抽象的詞,字面上理解就是上下層的傳遞,上會(huì)把內(nèi)容傳遞給下,在go中程序單位一般為goroutine,這里的上下文便是在goroutine之間進(jìn)行傳遞。

根據(jù)現(xiàn)實(shí)例子來講,最??吹絚ontext的便是web端。一個(gè)網(wǎng)絡(luò)請求request請求服務(wù)端,每一個(gè)request都會(huì)開啟一個(gè)goroutine,這個(gè)goroutine在邏輯處理中可能會(huì)去開啟其他的goroutine,例如去開啟一個(gè)MongoDB的連接,一個(gè)request的goroutine開啟了很多個(gè)goroutine時(shí)候,需要對這些goroutine進(jìn)行控制,這時(shí)候就需要context來進(jìn)行對這些goroutine進(jìn)行跟蹤。即一個(gè)請求Request,會(huì)需要多個(gè)Goroutine中處理。而這些Goroutine可能需要共享Request的一些信息;同時(shí)當(dāng)Request被取消或者超時(shí)的時(shí)候,所有從這個(gè)Request創(chuàng)建的所有Goroutine也應(yīng)該被結(jié)束。

例子講述完畢,用go的風(fēng)格再講一次。

在每一個(gè)goroutine在執(zhí)行之前,都要知道程序當(dāng)前的執(zhí)行狀態(tài),這些狀態(tài)都被封裝在context變量中,要傳遞給要執(zhí)行的goroutine中去,這個(gè)上下文就成為了傳遞與請求同生存周期變量的標(biāo)準(zhǔn)方法。

注意 context是在go 1.7版本之后引入的,以前版本的注意(go更新特別快,每一個(gè)版本都變得越來越好,自己第一次接觸go語言的時(shí)候才1.9版本,實(shí)習(xí)公司用的好像是1.7,研發(fā)團(tuán)隊(duì)解體后現(xiàn)在實(shí)習(xí)用的版本是1.11 短時(shí)間版本就如此之大,1.10版本G-M模型改為G-P-M模型,聽聞1.12社區(qū)會(huì)再次優(yōu)化GC垃圾回收,引入分代)


Context接口
Context的接口定義的比較簡潔,我們看下這個(gè)接口的方法。

type Context interface {
    Deadline() (deadline time.Time, ok bool)

    Done() <-chan struct{}

    Err() error

    Value(key interface{}) interface{}
}

這個(gè)接口共有4個(gè)方法,了解這些方法的意思非常重要,這樣我們才可以更好的使用他們。

Deadline方法是獲取設(shè)置的截止時(shí)間的意思,第一個(gè)返回式是截止時(shí)間,到了這個(gè)時(shí)間點(diǎn),Context會(huì)自動(dòng)發(fā)起取消請求;第二個(gè)返回值ok==false時(shí)表示沒有設(shè)置截止時(shí)間,如果需要取消的話,需要調(diào)用取消函數(shù)進(jìn)行取消。

Done方法返回一個(gè)只讀的chan,類型為struct{},我們在goroutine中,如果該方法返回的chan可以讀取,則意味著parent context已經(jīng)發(fā)起了取消請求,我們通過Done方法收到這個(gè)信號(hào)后,就應(yīng)該做清理操作,然后退出goroutine,釋放資源。

Err方法返回取消的錯(cuò)誤原因,因?yàn)槭裁碈ontext被取消。

Value方法獲取該Context上綁定的值,是一個(gè)鍵值對,所以要通過一個(gè)Key才可以獲取對應(yīng)的值,這個(gè)值一般是線程安全的。

有了如上的根Context,那么是如何衍生更多的子Context的呢?這就要靠context包為我們提供的With系列的函數(shù)了。

Context的繼承衍生

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, val interface{}) Context

這四個(gè)With函數(shù),接收的都有一個(gè)partent參數(shù),就是父Context,我們要基于這個(gè)父Context創(chuàng)建出子Context的意思,這種方式可以理解為子Context對父Context的繼承,也可以理解為基于父Context的衍生。

通過這些函數(shù),就創(chuàng)建了一顆Context樹,樹的每個(gè)節(jié)點(diǎn)都可以有任意多個(gè)子節(jié)點(diǎn),節(jié)點(diǎn)層級可以有任意多個(gè)。

WithCancel函數(shù),傳遞一個(gè)父Context作為參數(shù),返回子Context,以及一個(gè)取消函數(shù)用來取消Context。 WithDeadline函數(shù),和WithCancel差不多,它會(huì)多傳遞一個(gè)截止時(shí)間參數(shù),意味著到了這個(gè)時(shí)間點(diǎn),會(huì)自動(dòng)取消Context,當(dāng)然我們也可以不等到這個(gè)時(shí)候,可以提前通過取消函數(shù)進(jìn)行取消。

WithTimeoutWithDeadline基本上一樣,這個(gè)表示是超時(shí)自動(dòng)取消,是多少時(shí)間后自動(dòng)取消Context的意思。

WithValue函數(shù)和取消Context無關(guān),它是為了生成一個(gè)綁定了一個(gè)鍵值對數(shù)據(jù)的Context,這個(gè)綁定的數(shù)據(jù)可以通過Context.Value方法訪問到


引用飛雪無情的代碼:

func main() {
    ctx, cancel := context.WithCancel(context.Background())
    go func(ctx context.Context) {
        for {
            select {
            case <-ctx.Done():
                fmt.Println("監(jiān)控退出,停止了...")
                return
            default:
                fmt.Println("goroutine監(jiān)控中...")
                time.Sleep(2 * time.Second)
            }
        }
    }(ctx)

    time.Sleep(10 * time.Second)
    fmt.Println("可以了,通知監(jiān)控停止")
    cancel()
    //為了檢測監(jiān)控過是否停止,如果沒有監(jiān)控輸出,就表示停止了
    time.Sleep(5 * time.Second)

}

context.Background() 返回一個(gè)空的Context,這個(gè)空的Context一般用于整個(gè)Context樹的根節(jié)點(diǎn)。然后我們使用context.WithCancel(parent)函數(shù),創(chuàng)建一個(gè)可取消的子Context,然后當(dāng)作參數(shù)傳給goroutine使用,這樣就可以使用這個(gè)子Context跟蹤這個(gè)goroutine。

在goroutine中,使用select調(diào)用<-ctx.Done()判斷是否要結(jié)束,如果接受到值的話,就可以返回結(jié)束goroutine了;如果接收不到,就會(huì)繼續(xù)進(jìn)行監(jiān)控。

那么是如何發(fā)送結(jié)束指令的呢?這就是示例中的cancel函數(shù)啦,它是我們調(diào)用context.WithCancel(parent)函數(shù)生成子Context的時(shí)候返回的,第二個(gè)返回值就是這個(gè)取消函數(shù),它是CancelFunc類型的。我們調(diào)用它就可以發(fā)出取消指令,然后我們的監(jiān)控goroutine就會(huì)收到信號(hào),就會(huì)返回結(jié)束。

在引用一段多控制

func main() {
    ctx, cancel := context.WithCancel(context.Background())
    go watch(ctx,"【監(jiān)控1】")
    go watch(ctx,"【監(jiān)控2】")
    go watch(ctx,"【監(jiān)控3】")

    time.Sleep(10 * time.Second)
    fmt.Println("可以了,通知監(jiān)控停止")
    cancel()
    //為了檢測監(jiān)控過是否停止,如果沒有監(jiān)控輸出,就表示停止了
    time.Sleep(5 * time.Second)
}

func watch(ctx context.Context, name string) {
    for {
        select {
        case <-ctx.Done():
            fmt.Println(name,"監(jiān)控退出,停止了...")
            return
        default:
            fmt.Println(name,"goroutine監(jiān)控中...")
            time.Sleep(2 * time.Second)
        }
    }
}

示例中啟動(dòng)了3個(gè)監(jiān)控goroutine進(jìn)行不斷的監(jiān)控,每一個(gè)都使用了Context進(jìn)行跟蹤,當(dāng)我們使用cancel函數(shù)通知取消時(shí),這3個(gè)goroutine都會(huì)被結(jié)束。這就是Context的控制能力,它就像一個(gè)控制器一樣,按下開關(guān)后,所有基于這個(gè)Context或者衍生的子Context都會(huì)收到通知,這時(shí)就可以進(jìn)行清理操作了,最終釋放goroutine,這就優(yōu)雅的解決了goroutine啟動(dòng)后不可控的問題。


在引用一次潘少大佬的代碼:

package main

import (
    "context"
    "crypto/md5"
    "fmt"
    "io/ioutil"
    "net/http"
    "sync"
    "time"
)

type favContextKey string

func main() {
    wg := &sync.WaitGroup{}
    values := []string{"https://www.baidu.com/", "https://www.zhihu.com/"}
    ctx, cancel := context.WithCancel(context.Background())

    for _, url := range values {
        wg.Add(1)
        subCtx := context.WithValue(ctx, favContextKey("url"), url)
        go reqURL(subCtx, wg)
    }

    go func() {
        time.Sleep(time.Second * 3)
        cancel()
    }()

    wg.Wait()
    fmt.Println("exit main goroutine")
}

func reqURL(ctx context.Context, wg *sync.WaitGroup) {
    defer wg.Done()
    url, _ := ctx.Value(favContextKey("url")).(string)
    for {
        select {
        case <-ctx.Done():
            fmt.Printf("stop getting url:%s\n", url)
            return
        default:
            r, err := http.Get(url)
            if r.StatusCode == http.StatusOK && err == nil {
                body, _ := ioutil.ReadAll(r.Body)
                subCtx := context.WithValue(ctx, favContextKey("resp"), fmt.Sprintf("%s%x", url, md5.Sum(body)))
                wg.Add(1)
                go showResp(subCtx, wg)
            }
            r.Body.Close()
            //啟動(dòng)子goroutine是為了不阻塞當(dāng)前goroutine,這里在實(shí)際場景中可以去執(zhí)行其他邏輯,這里為了方便直接sleep一秒
            // doSometing()
            time.Sleep(time.Second * 1)
        }
    }
}

func showResp(ctx context.Context, wg *sync.WaitGroup) {
    defer wg.Done()
    for {
        select {
        case <-ctx.Done():
            fmt.Println("stop showing resp")
            return
        default:
            //子goroutine里一般會(huì)處理一些IO任務(wù),如讀寫數(shù)據(jù)庫或者rpc調(diào)用,這里為了方便直接把數(shù)據(jù)打印
            fmt.Println("printing ", ctx.Value(favContextKey("resp")))
            time.Sleep(time.Second * 1)
        }
    }
}

首先調(diào)用context.Background()生成根節(jié)點(diǎn),然后調(diào)用withCancel方法,傳入根節(jié)點(diǎn),得到新的子Context以及根節(jié)點(diǎn)的cancel方法(通知所有子節(jié)點(diǎn)結(jié)束運(yùn)行),這里要注意:該方法也返回了一個(gè)Context,這是一個(gè)新的子節(jié)點(diǎn),與初始傳入的根節(jié)點(diǎn)不是同一個(gè)實(shí)例了,但是每一個(gè)子節(jié)點(diǎn)里會(huì)保存從最初的根節(jié)點(diǎn)到本節(jié)點(diǎn)的鏈路信息 ,才能實(shí)現(xiàn)鏈?zhǔn)健?/p>

程序的reqURL方法接收一個(gè)url,然后通過http請求該url獲得response,然后在當(dāng)前goroutine里再啟動(dòng)一個(gè)子groutine把response打印出來,然后從ReqURL開始Context樹往下衍生葉子節(jié)點(diǎn)(每一個(gè)鏈?zhǔn)秸{(diào)用新產(chǎn)生的ctx),中間每個(gè)ctx都可以通過WithValue方式傳值(實(shí)現(xiàn)通信),而每一個(gè)子goroutine都能通過Value方法從父goroutine取值,實(shí)現(xiàn)協(xié)程間的通信,每個(gè)子ctx可以調(diào)用Done方法檢測是否有父節(jié)點(diǎn)調(diào)用cancel方法通知子節(jié)點(diǎn)退出運(yùn)行,根節(jié)點(diǎn)的cancel調(diào)用會(huì)沿著鏈路通知到每一個(gè)子節(jié)點(diǎn),因此實(shí)現(xiàn)了強(qiáng)并發(fā)控制,流程如圖:

044svco84sif9rjebqagmar0fp.png

context使用規(guī)范

最后,Context雖然是神器,但開發(fā)者使用也要遵循基本法,以下是一些Context使用的規(guī)范:

  • Do not store Contexts inside a struct type; instead, pass a Context explicitly to each function that needs it. The Context should be the first parameter, typically named ctx;不要把Context存在一個(gè)結(jié)構(gòu)體當(dāng)中,顯式地傳入函數(shù)。Context變量需要作為第一個(gè)參數(shù)使用,一般命名為ctx;
  • Do not pass a nil Context, even if a function permits it. Pass context.TODO if you are unsure about which Context to use;即使方法允許,也不要傳入一個(gè)nil的Context,如果你不確定你要用什么Context的時(shí)候傳一個(gè)context.TODO;
  • Use context Values only for request-scoped data that transits processes and APIs, not for passing optional parameters to functions;使用context的Value相關(guān)方法只應(yīng)該用于在程序和接口中傳遞的和請求相關(guān)的元數(shù)據(jù),不要用它來傳遞一些可選的參數(shù);
  • The same Context may be passed to functions running in different goroutines; Contexts are safe for simultaneous use by multiple goroutines;同樣的Context可以用來傳遞到不同的goroutine中,Context在多個(gè)goroutine中是安全的;
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

友情鏈接更多精彩內(nèi)容