Golang中 Context包深入淺出

控制并發(fā)有兩種經典的方式袄简,一種是WaitGroup腥放,另外一種就是Context,今天我就談談Context绿语。

什么是WaitGroup

WaitGroup以前我們在并發(fā)的時候介紹過秃症,它是一種控制并發(fā)的方式,它的這種方式是控制多個goroutine同時完成吕粹。

func main() {
    var wg sync.WaitGroup

    wg.Add(2)
    go func() {
        time.Sleep(2*time.Second)
        fmt.Println("1號完成")
        wg.Done()
    }()
    go func() {
        time.Sleep(2*time.Second)
        fmt.Println("2號完成")
        wg.Done()
    }()
    wg.Wait()
    fmt.Println("好了种柑,大家都干完了,放工")
}

一個很簡單的例子匹耕,一定要例子中的2個goroutine同時做完聚请,才算是完成,先做好的就要等著其他未完成的稳其,所有的goroutine要都全部完成才可以驶赏。

這是一種控制并發(fā)的方式,這種尤其適用于既鞠,好多個goroutine協(xié)同做一件事情的時候煤傍,因為每個goroutine做的都是這件事情的一部分,只有全部的goroutine都完成嘱蛋,這件事情才算是完成蚯姆,這是等待的方式。

在實際的業(yè)務種洒敏,我們可能會有這么一種場景:需要我們主動的通知某一個goroutine結束龄恋。比如我們開啟一個后臺goroutine一直做事情,比如監(jiān)控凶伙,現在不需要了郭毕,就需要通知這個監(jiān)控goroutine結束,不然它會一直跑函荣,就泄漏了显押。

chan通知

我們都知道一個goroutine啟動后链韭,我們是無法控制他的,大部分情況是等待它自己結束煮落,那么如果這個goroutine是一個不會自己結束的后臺goroutine呢?比如監(jiān)控等踊谋,會一直運行的蝉仇。

這種情況化,一直傻瓜式的辦法是全局變量殖蚕,其他地方通過修改這個變量完成結束通知轿衔,然后后臺goroutine不停的檢查這個變量,如果發(fā)現被通知關閉了睦疫,就自我結束害驹。

這種方式也可以,但是首先我們要保證這個變量在多線程下的安全蛤育,基于此宛官,有一種更好的方式:chan + select 。

func main() {
    stop := make(chan bool)

    go func() {
        for {
            select {
            case <-stop:
                fmt.Println("監(jiān)控退出瓦糕,停止了...")
                return
            default:
                fmt.Println("goroutine監(jiān)控中...")
                time.Sleep(2 * time.Second)
            }
        }
    }()

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

}

例子中我們定義一個stop的chan亥揖,通知他結束后臺goroutine。實現也非常簡單圣勒,在后臺goroutine中费变,使用select判斷stop是否可以接收到值,如果可以接收到圣贸,就表示可以退出停止了挚歧;如果沒有接收到,就會執(zhí)行default里的監(jiān)控邏輯旁趟,繼續(xù)監(jiān)控昼激,只到收到stop的通知。

有了以上的邏輯锡搜,我們就可以在其他goroutine種橙困,給stop chan發(fā)送值了,例子中是在main goroutine中發(fā)送的耕餐,控制讓這個監(jiān)控的goroutine結束凡傅。

發(fā)送了stop<- true結束的指令后,我這里使用time.Sleep(5 * time.Second)故意停頓5秒來檢測我們結束監(jiān)控goroutine是否成功肠缔。如果成功的話夏跷,不會再有goroutine監(jiān)控中...的輸出了哼转;如果沒有成功,監(jiān)控goroutine就會繼續(xù)打印goroutine監(jiān)控中...輸出槽华。

這種chan+select的方式壹蔓,是比較優(yōu)雅的結束一個goroutine的方式,不過這種方式也有局限性猫态,如果有很多goroutine都需要控制結束怎么辦呢佣蓉?如果這些goroutine又衍生了其他更多的goroutine怎么辦呢?如果一層層的無窮盡的goroutine呢亲雪?這就非常復雜了勇凭,即使我們定義很多chan也很難解決這個問題,因為goroutine的關系鏈就導致了這種場景非常復雜义辕。

初識Context

上面說的這種場景是存在的虾标,比如一個網絡請求Request,每個Request都需要開啟一個goroutine做一些事情灌砖,這些goroutine又可能會開啟其他的goroutine璧函。所以我們需要一種可以跟蹤goroutine的方案,才可以達到控制他們的目的周崭,這就是Go語言為我們提供的Context柳譬,稱之為上下文非常貼切,它就是goroutine的上下文续镇。

下面我們就使用Go Context重寫上面的示例美澳。

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)

}

重寫比較簡單酱虎,就是把原來的chan stop 換成Context雨膨,使用Context跟蹤goroutine,以便進行控制读串,比如結束等聊记。

context.Background() 返回一個空的Context,這個空的Context一般用于整個Context樹的根節(jié)點恢暖。然后我們使用context.WithCancel(parent)函數排监,創(chuàng)建一個可取消的子Context,然后當作參數傳給goroutine使用杰捂,這樣就可以使用這個子Context跟蹤這個goroutine舆床。

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

那么是如何發(fā)送結束指令的呢湿弦?這就是示例中的cancel函數啦,它是我們調用context.WithCancel(parent)函數生成子Context的時候返回的腾夯,第二個返回值就是這個取消函數省撑,它是CancelFunc類型的。我們調用它就可以發(fā)出取消指令俯在,然后我們的監(jiān)控goroutine就會收到信號,就會返回結束娃惯。

Context控制多個goroutine

使用Context控制一個goroutine的例子如上跷乐,非常簡單,下面我們看看控制多個goroutine的例子趾浅,其實也比較簡單愕提。

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)
        }
    }
}

示例中啟動了3個監(jiān)控goroutine進行不斷的監(jiān)控证膨,每一個都使用了Context進行跟蹤如输,當我們使用cancel函數通知取消時,這3個goroutine都會被結束央勒。這就是Context的控制能力不见,它就像一個控制器一樣,按下開關后崔步,所有基于這個Context或者衍生的子Context都會收到通知稳吮,這時就可以進行清理操作了,最終釋放goroutine井濒,這就優(yōu)雅的解決了goroutine啟動后不可控的問題灶似。

Context接口

Context的接口定義的比較簡潔,我們看下這個接口的方法瑞你。

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

    Done() <-chan struct{}

    Err() error

    Value(key interface{}) interface{}
}

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

Deadline方法是獲取設置的截止時間的意思撞蚕,第一個返回式是截止時間,到了這個時間點过牙,Context會自動發(fā)起取消請求甥厦;第二個返回值ok==false時表示沒有設置截止時間纺铭,如果需要取消的話,需要調用取消函數進行取消刀疙。

Done方法返回一個只讀的chan舶赔,類型為struct{},我們在goroutine中谦秧,如果該方法返回的chan可以讀取竟纳,則意味著parent context已經發(fā)起了取消請求,我們通過Done方法收到這個信號后疚鲤,就應該做清理操作锥累,然后退出goroutine,釋放資源集歇。

Err方法返回取消的錯誤原因桶略,因為什么Context被取消。

Value方法獲取該Context上綁定的值诲宇,是一個鍵值對际歼,所以要通過一個Key才可以獲取對應的值,這個值一般是線程安全的姑蓝。

以上四個方法中常用的就是Done了鹅心,如果Context取消的時候,我們就可以得到一個關閉的chan纺荧,關閉的chan是可以讀取的旭愧,所以只要可以讀取的時候,就意味著收到Context取消的信號了宙暇,以下是這個方法的經典用法榕茧。

  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:
        }
    }
  }

Context接口并不需要我們實現,Go內置已經幫我們實現了2個客给,我們代碼中最開始都是以這兩個內置的作為最頂層的partent context用押,衍生出更多的子Context。

var (
    background = new(emptyCtx)
    todo       = new(emptyCtx)
)

func Background() Context {
    return background
}

func TODO() Context {
    return todo
}

一個是Background靶剑,主要用于main函數蜻拨、初始化以及測試代碼中,作為Context這個樹結構的最頂層的Context桩引,也就是根Context缎讼。

一個是TODO,它目前還不知道具體的使用場景,如果我們不知道該使用什么Context的時候坑匠,可以使用這個血崭。

他們兩個本質上都是emptyCtx結構體類型,是一個不可取消,沒有設置截止時間夹纫,沒有攜帶任何值的Context咽瓷。

type emptyCtx int

func (*emptyCtx) Deadline() (deadline time.Time, ok bool) {
    return
}

func (*emptyCtx) Done() <-chan struct{} {
    return nil
}

func (*emptyCtx) Err() error {
    return nil
}

func (*emptyCtx) Value(key interface{}) interface{} {
    return nil
}

這就是emptyCtx實現Context接口的方法,可以看到舰讹,這些方法什么都沒做茅姜,返回的都是nil或者零值。

Context的繼承衍生

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

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

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

通過這些函數退腥,就創(chuàng)建了一顆Context樹,樹的每個節(jié)點都可以有任意多個子節(jié)點再榄,節(jié)點層級可以有任意多個狡刘。

WithCancel函數,傳遞一個父Context作為參數困鸥,返回子Context嗅蔬,以及一個取消函數用來取消Context。WithDeadline函數疾就,和WithCancel差不多澜术,它會多傳遞一個截止時間參數,意味著到了這個時間點猬腰,會自動取消Context鸟废,當然我們也可以不等到這個時候,可以提前通過取消函數進行取消姑荷。

WithTimeoutWithDeadline基本上一樣盒延,這個表示是超時自動取消,是多少時間后自動取消Context的意思鼠冕。

WithValue函數和取消Context無關添寺,它是為了生成一個綁定了一個鍵值對數據的Context,這個綁定的數據可以通過Context.Value方法訪問到懈费,后面我們會專門講计露。

大家可能留意到,前三個函數都返回一個取消函數CancelFunc,這是一個函數類型票罐,它的定義非常簡單叉趣。

type CancelFunc func()

這就是取消函數的類型,該函數可以取消一個Context胶坠,以及這個節(jié)點Context下所有的所有的Context君账,不管有多少層級。

WithValue傳遞元數據

通過Context我們也可以傳遞一些必須的元數據沈善,這些數據會附加在Context上以供使用乡数。

var key string="name"

func main() {
    ctx, cancel := context.WithCancel(context.Background())
    //附加值
    valueCtx:=context.WithValue(ctx,key,"【監(jiān)控1】")
    go watch(valueCtx)
    time.Sleep(10 * time.Second)
    fmt.Println("可以了,通知監(jiān)控停止")
    cancel()
    //為了檢測監(jiān)控過是否停止闻牡,如果沒有監(jiān)控輸出净赴,就表示停止了
    time.Sleep(5 * time.Second)
}

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

在前面的例子罩润,我們通過傳遞參數的方式玖翅,把name的值傳遞給監(jiān)控函數。在這個例子里割以,我們實現一樣的效果金度,但是通過的是Context的Value的方式。

我們可以使用context.WithValue方法附加一對K-V的鍵值對严沥,這里Key必須是等價性的猜极,也就是具有可比性;Value值要是線程安全的消玄。

這樣我們就生成了一個新的Context跟伏,這個新的Context帶有這個鍵值對,在使用的時候翩瓜,可以通過Value方法讀取ctx.Value(key)受扳。

記住,使用WithValue傳值兔跌,一般是必須的值勘高,不要什么值都傳遞。

Context 使用原則

  1. 不要把Context放在結構體中坟桅,要以參數的方式傳遞
  2. 以Context作為參數的函數方法相满,應該把Context作為第一個參數,放在第一位桦卒。
  3. 給一個函數方法傳遞Context的時候立美,不要傳遞nil,如果不知道傳遞什么方灾,就使用context.TODO
  4. Context的Value相關方法應該傳遞必須的數據建蹄,不要什么數據都使用這個傳遞
  5. Context是縣城安全的碌更,可以放心的在多個goroutine中傳遞
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市洞慎,隨后出現的幾起案子痛单,更是在濱河造成了極大的恐慌,老刑警劉巖劲腿,帶你破解...
    沈念sama閱讀 217,185評論 6 503
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件旭绒,死亡現場離奇詭異,居然都是意外死亡焦人,警方通過查閱死者的電腦和手機挥吵,發(fā)現死者居然都...
    沈念sama閱讀 92,652評論 3 393
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來花椭,“玉大人忽匈,你說我怎么就攤上這事】罅桑” “怎么了丹允?”我有些...
    開封第一講書人閱讀 163,524評論 0 353
  • 文/不壞的土叔 我叫張陵,是天一觀的道長袋倔。 經常有香客問我雕蔽,道長,這世上最難降的妖魔是什么宾娜? 我笑而不...
    開封第一講書人閱讀 58,339評論 1 293
  • 正文 為了忘掉前任批狐,我火速辦了婚禮,結果婚禮上碳默,老公的妹妹穿的比我還像新娘。我一直安慰自己缘眶,他們只是感情好嘱根,可當我...
    茶點故事閱讀 67,387評論 6 391
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著巷懈,像睡著了一般该抒。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上顶燕,一...
    開封第一講書人閱讀 51,287評論 1 301
  • 那天凑保,我揣著相機與錄音,去河邊找鬼涌攻。 笑死欧引,一個胖子當著我的面吹牛,可吹牛的內容都是我干的恳谎。 我是一名探鬼主播芝此,決...
    沈念sama閱讀 40,130評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼憋肖,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了婚苹?” 一聲冷哼從身側響起岸更,我...
    開封第一講書人閱讀 38,985評論 0 275
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎膊升,沒想到半個月后怎炊,有當地人在樹林里發(fā)現了一具尸體,經...
    沈念sama閱讀 45,420評論 1 313
  • 正文 獨居荒郊野嶺守林人離奇死亡廓译,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 37,617評論 3 334
  • 正文 我和宋清朗相戀三年评肆,在試婚紗的時候發(fā)現自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片责循。...
    茶點故事閱讀 39,779評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡糟港,死狀恐怖,靈堂內的尸體忽然破棺而出院仿,到底是詐尸還是另有隱情秸抚,我是刑警寧澤,帶...
    沈念sama閱讀 35,477評論 5 345
  • 正文 年R本政府宣布歹垫,位于F島的核電站剥汤,受9級特大地震影響,放射性物質發(fā)生泄漏排惨。R本人自食惡果不足惜吭敢,卻給世界環(huán)境...
    茶點故事閱讀 41,088評論 3 328
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望暮芭。 院中可真熱鬧鹿驼,春花似錦、人聲如沸辕宏。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,716評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽瑞筐。三九已至凄鼻,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間聚假,已是汗流浹背块蚌。 一陣腳步聲響...
    開封第一講書人閱讀 32,857評論 1 269
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留膘格,地道東北人峭范。 一個月前我還...
    沈念sama閱讀 47,876評論 2 370
  • 正文 我出身青樓,卻偏偏與公主長得像瘪贱,于是被迫代替她去往敵國和親虎敦。 傳聞我的和親對象是個殘疾皇子游岳,可洞房花燭夜當晚...
    茶點故事閱讀 44,700評論 2 354

推薦閱讀更多精彩內容