中文字幕av专区_日韩电影在线播放_精品国产精品久久一区免费式_av在线免费观看网站

溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點擊 登錄注冊 即表示同意《億速云用戶服務條款》

Go語言之Context

發布時間:2020-07-27 06:34:47 來源:網絡 閱讀:830 作者:baby神 欄目:編程語言

控制并發有兩種經典的方式,一種是WaitGroup,另外一種就是Context,今天我就談談Context。



什么是WaitGroup



WaitGroup以前我們在并發的時候介紹過,它是一種控制并發的方式,它的這種方式是控制多個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("好了,大家都干完了,放工")
}

 

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



這是一種控制并發的方式,這種尤其適用于,好多個goroutine協同做一件事情的時候,因為每個goroutine做的都是這件事情的一部分,只有全部的goroutine都完成,這件事情才算是完成,這是等待的方式。



在實際的業務種,我們可能會有這么一種場景:需要我們主動的通知某一個goroutine結束。比如我們開啟一個后臺goroutine一直做事情,比如監控,現在不需要了,就需要通知這個監控goroutine結束,不然它會一直跑,就泄漏了。



chan通知



我們都知道一個goroutine啟動后,我們是無法控制他的,大部分情況是等待它自己結束,那么如果這個goroutine是一個不會自己結束的后臺goroutine呢?比如監控等,會一直運行的。



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



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



func main() {

   stop := make(chan bool)

   go func() {

           for {

                     select {

                               case <-stop:

                                     fmt.Println("監控退出,停止了...")                                            return

                            default:

                                     fmt.Println("goroutine監控中...")

                                     time.Sleep(2 * time.Second)

                        }

              }

    }()

 

   time.Sleep(10 * time.Second)

   fmt.Println("可以了,通知監控停止")

   stop<- true

   //為了檢測監控過是否停止,如果沒有監控輸出,就表示停止了

   time.Sleep(5 * time.Second)
}



例子中我們定義一個stop的chan,通知他結束后臺goroutine。實現也非常簡單,在后臺goroutine中,使用select判斷stop是否可以接收到值,如果可以接收到,就表示可以退出停止了;如果沒有接收到,就會執行default里的監控邏輯,繼續監控,只到收到stop的通知。



有了以上的邏輯,我們就可以在其他goroutine種,給stop chan發送值了,例子中是在maingoroutine中發送的,控制讓這個監控的goroutine結束。



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



這種chan+select的方式,是比較優雅的結束一個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("監控退出,停止了...")               

               return

           default:

               fmt.Println("goroutine監控中...")

               time.Sleep(2 * time.Second)

           }

       }

   }(ctx)

 

   time.Sleep(10 * time.Second)

   fmt.Println("可以了,通知監控停止")

   cancel()    

   //為了檢測監控過是否停止,如果沒有監控輸出,就表示停止了

   time.Sleep(5 * time.Second)
}



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



context.Background()返回一個空的Context,這個空的Context一般用于整個Context樹的根節點。然后我們使用context.WithCancel(parent)函數,創建一個可取消的子Context,然后當作參數傳給goroutine使用,這樣就可以使用這個子Context跟蹤這個goroutine。



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



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



Context控制多個goroutine



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



func main() {

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

   go watch(ctx,"【監控1")    

   go watch(ctx,"【監控2")    

   go watch(ctx,"【監控3")

 

   time.Sleep(10 * time.Second)

   fmt.Println("可以了,通知監控停止")

   cancel()    

   //為了檢測監控過是否停止,如果沒有監控輸出,就表示停止了

   time.Sleep(5 * time.Second)}func watch(ctx context.Context, name string) {

   for {       

       select {        

           case <-ctx.Done():

           fmt.Println(name,"監控退出,停止了...")         

            return

       default:

           fmt.Println(name,"goroutine監控中...")

           time.Sleep(2 * time.Second)

       }

   }
}



示例中啟動了 3 個監控goroutine進行不斷的監控,每一個都使用了Context進行跟蹤,當我們使用cancel函數通知取消時,這 3 個goroutine都會被結束。這就是Context的控制能力,它就像一個控制器一樣,按下開關后,所有基于這個Context或者衍生的子Context都會收到通知,這時就可以進行清理操作了,最終釋放goroutine,這就優雅的解決了goroutine啟動后不可控的問題。

 

Context接口

 

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

 

type Contextinterface {
  
Deadline() (deadline time.Time, ok bool)
  
Done() <-chan struct{}
  
Err() error  

  Valu (key interface{}) interface{}
}



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



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



Done方法返回一個只讀的chan,類型為struct{},我們在goroutine中,如果該方法返回的chan可以讀取,則意味著parent context已經發起了取消請求,我們通過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{} {

   returnnil
}
func (*emptyCtx)
Err() error {

   returnnil
}
func (*emptyCtx)
Value(key interface{}) interface{} {

   returnnil
}



這就是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創建出子Context的意思,這種方式可以理解為子Context對父Context的繼承,也可以理解為基于父Context的衍生。



通過這些函數,就創建了一顆Context樹,樹的每個節點都可以有任意多個子節點,節點層級可以有任意多個。



WithCancel函數,傳遞一個父Context作為參數,返回子Context,以及一個取消函數用來取消Context。

 

 

WithDeadline函數,和WithCancel差不多,它會多傳遞一個截止時間參數,意味著到了這個時間點,會自動取消Context,當然我們也可以不等到這個時候,可以提前通過取消函數進行取消。



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



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



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



type CancelFunc func()



這就是取消函數的類型,該函數可以取消一個Context,以及這個節點Context下所有的所有的Context,不管有多少層級。



WithValue傳遞元數據



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



var key string="name"func main() {

   ctx, cancel := context.WithCancel(context.Background())    //附加值

   valueCtx:=context.WithValue(ctx,key,"【監控1")

   go watch(valueCtx)

   time.Sleep(10 * time.Second)

   fmt.Println("可以了,通知監控停止")

   cancel()    

   //為了檢測監控過是否停止,如果沒有監控輸出,就表示停止了

   time.Sleep(5 * time.Second)}func watch(ctx context.Context) {

   for {       

       select {        

           case <-ctx.Done():           

           //取出值

           fmt.Println(ctx.Value(key),"監控退出,停止了...")           

           return

       default:            

           //取出值

           fmt.Println(ctx.Value(key),"goroutine監控中...")

           time.Sleep(2 * time.Second)

       }

   }
}



在前面的例子,我們通過傳遞參數的方式,把name的值傳遞給監控函數。在這個例子里,我們實現一樣的效果,但是通過的是Context的Value的方式。



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



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


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



Context 使用原則



·        不要把Context放在結構體中,要以參數的方式傳遞。



·        以Context作為參數的函數方法,應該把Context作為第一個參數,放在第一位。



·        給一個函數方法傳遞Context的時候,不要傳遞nil,如果不知道傳遞什么,就使用context.TODO。



·        Context的Value相關方法應該傳遞必須的數據,不要什么數據都使用這個傳遞。



·        Context是縣城安全的,可以放心的在多個goroutine中傳遞。

 


向AI問一下細節

免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。

AI

拉萨市| 临湘市| 呼和浩特市| 阳谷县| 乐安县| 绥化市| 福贡县| 罗田县| 汤阴县| 长宁县| 康定县| 衡山县| 汉沽区| 城固县| 塔河县| 门源| 布拖县| 东方市| 新安县| 桓仁| 抚远县| 革吉县| 古交市| 资源县| 永顺县| 余庆县| 潼关县| 大同市| 甘泉县| 黄陵县| 澎湖县| 通化县| 金寨县| 大邑县| 比如县| 和静县| 天全县| 东兰县| 大连市| 阜康市| 晋宁县|