Go 併發控制

前言

提到Go語言的併發,就不得不提goroutine,其作爲Go語言的一大特色,在日常開發中使用很多。

在日常使用場景就會涉及一個goroutine啓動或結束,啓動一個goroutine很簡單只需要前面加關鍵詞go即可,而由於每個goroutine都是獨立運行的,其退出有自身決定的,除非main主程序結束或程序崩潰的情況發生。

那麼,如何控制goroutine或者說通知goroutine結束運行呢?

解決的方式其實很簡單,那就是想辦法和goroutine通訊,通知goroutine什麼時候結束,goroutine結束也可以通知其他goroutine或main主程序。

併發控制方法主要有:

全局變量

channel

WaitGroup

context

全局變量

這是併發控制最簡單的實現方式

1、聲明一個全局變量。

2、所有子goroutine共享這個變量,並不斷輪詢這個變量檢查是否有更新;

3、在主進程中變更該全局變量;

4、子goroutine檢測到全局變量更新,執行相應的邏輯。

示例

package main

import (
   "fmt"
   "time"
)

func main() {
   open := true
   go func() {
      for open {
         println("goroutineA running")
         time.Sleep(1 * time.Second)
      }
      println("goroutineA exit")
   }()
   go func() {
      for open {
         println("goroutineB running")
         time.Sleep(1 * time.Second)
      }
      println("goroutineB exit")
   }()
   time.Sleep(2 * time.Second)
   open = false
   time.Sleep(2 * time.Second)
   fmt.Println("main fun exit")
}

輸出

goroutineA running
goroutineB running
goroutineA running
goroutineB running
goroutineB running
goroutineA exit
goroutineB exit
main fun exit

這種實現方式

優點:實現簡單。

缺點:適用一些邏輯簡單的場景,全局變量的信息量比較少,爲了防止不同goroutine同時修改變量需要用到加鎖來結局。

channel

channel是goroutine之間主要的通訊方式,一般會和select搭配使用。

如想了解channel實現原理可參考

https://github.com/guyan0319/...

1、聲明一個stop的chan。

2、在goroutine中,使用select判斷stop是否可以接收到值,如果可以接收到,就表示可以退出停止了;如果沒有接收到,就會執行default裏邏輯。直到收到stop的通知。

3、主程序發送了stop<- true結束的指令後。

4、子goroutine接到結束指令case <-stop退出return。

示例

package main

import (
   "fmt"
   "time"
)

func main() {
   stop := make(chan bool)
   go func() {
      for {
         select {
         case <-stop:
            fmt.Println("goroutine exit")
            return
         default:
            fmt.Println("goroutine running")
            time.Sleep(1 * time.Second)
         }
      }
   }()
   time.Sleep(2 * time.Second)
   stop <- true
   time.Sleep(2 * time.Second)
   fmt.Println("main fun exit")
}

輸出

goroutine running
goroutine running
goroutine running
goroutine exit
main fun exit

這種select+chan是一種比較優雅的併發控制方式,但也有侷限性,如多個goroutine 需要結束,以及嵌套goroutine 的場景。

WaitGroup

Go語言提供同步包(sync),源碼(src/sync/waitgroup.go).

Sync包同步提供基本的同步原語,如互斥鎖。除了Once和WaitGroup類型之外,大多數類型都是供低級庫例程使用的。通過Channel和溝通可以更好地完成更高級別的同步。並且此包中的值在使用過後不要拷貝。

WaitGroup是一種實現併發控制方式,WaitGroup 對象內部有一個計數器,最初從0開始,它有三個方法:Add(), Done(), Wait() 用來控制計數器的數量。

  • Add(n) 把計數器設置爲n
  • Done() 每次把計數器-1
  • wait() 會阻塞代碼的運行,直到計數器地值減爲0。

示例

package main

import (
   "fmt"
   "sync"
   "time"
)

func main() {
   //定義一個WaitGroup
   var wg sync.WaitGroup
   //計數器設置爲2
   wg.Add(2)
   go func() {
      time.Sleep(2 * time.Second)
      fmt.Println("goroutineA finish")
      //計數器減1
      wg.Done()
   }()
   go func() {
      time.Sleep(2 * time.Second)
      fmt.Println("goroutineB finish")
      //計數器減1
      wg.Done()
   }()
   //會阻塞代碼的運行,直到計數器地值減爲0。
   wg.Wait()
   time.Sleep(2 * time.Second)
   fmt.Println("main fun exit")
}

這種控制併發的方式適用於,好多個goroutine協同做一件事情的時候,因爲每個goroutine做的都是這件事情的一部分,只有全部的goroutine都完成,這件事情纔算是完成,這是等待的方式。WaitGroup相對於channel併發控制方式比較輕巧。

注意:

1、計數器不能爲負值

2、WaitGroup對象不是一個引用類型

Context

應用場景:在 Go http 包的 Server 中,每個Request都需要開啓一個goroutine做一些事情,這些goroutine又可能會開啓其他的goroutine。所以我們需要一種可以跟蹤goroutine的方案,纔可以達到控制他們的目的,這就是Go語言爲我們提供的Context,稱之爲上下文。

控制併發的實現方式:

1、 context.Background():返回一個空的Context,這個空的Context一般用於整個Context樹的根節點。

2、context.WithCancel(context.Background()),創建一個可取消的子Context,然後當作參數傳給goroutine使用,這樣就可以使用這個子Context跟蹤這個goroutine。

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

4、cancel(),取消函數(context.WithCancel()返回的第二個參數,名字和你聲明的名字一致)。給goroutine發送結束指令。

示例:

package main

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

func main() {
   //創建一個可取消子context,context.Background():返回一個空的Context,這個空的Context一般用於整個Context樹的根節點。
   ctx, cancel := context.WithCancel(context.Background())
   go func(ctx context.Context) {
      for {
         select {
         //使用select調用<-ctx.Done()判斷是否要結束
         case <-ctx.Done():
            fmt.Println("goroutine exit")
            return
         default:
            fmt.Println("goroutine running.")
            time.Sleep(2 * time.Second)
         }
      }
   }(ctx)

   time.Sleep(10 * time.Second)
   fmt.Println("main fun exit")
   //取消context
   cancel()
   time.Sleep(5 * time.Second)

}

輸出:

goroutine running.
goroutine running.
goroutine running.
goroutine running.
goroutine running.
main fun exit
goroutine exit

如果想控制多個goroutine ,也很簡單。

示例

package main

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

func main() {
   //創建一個可取消子context,context.Background():返回一個空的Context,這個空的Context一般用於整個Context樹的根節點。
   ctx, cancel := context.WithCancel(context.Background())
   ctxTwo, cancelTwo := context.WithCancel(context.Background())
   go func(ctx context.Context) {
      for {
         select {
         //使用select調用<-ctx.Done()判斷是否要結束
         case <-ctx.Done():
            fmt.Println("goroutineA exit")
            return
         default:
            fmt.Println("goroutineA running.")
            time.Sleep(2 * time.Second)
         }
      }
   }(ctx)
   go func(ctx context.Context) {
      for {
         select {
         //使用select調用<-ctx.Done()判斷是否要結束
         case <-ctx.Done():
            fmt.Println("goroutineB exit")
            return
         default:
            fmt.Println("goroutineB running.")
            time.Sleep(2 * time.Second)
         }
      }
   }(ctx)
   go func(ctxTwo context.Context) {
      for {
         select {
         //使用select調用<-ctx.Done()判斷是否要結束
         case <-ctxTwo.Done():
            fmt.Println("goroutineC exit")
            return
         default:
            fmt.Println("goroutineC running.")
            time.Sleep(2 * time.Second)
         }
      }
   }(ctxTwo)

   time.Sleep(4 * time.Second)
   fmt.Println("main fun exit")
   //取消context
   cancel()
   cancelTwo()
   time.Sleep(5 * time.Second)

}

結果:

goroutineA running.
goroutineB running.
goroutineC running.
goroutineB running.
goroutineC running.
goroutineA running.
goroutineC running.
goroutineA running.
goroutineB running.
main fun exit
goroutineC exit
goroutineA exit
goroutineB exit

context還適用於更復雜的場景,如主動取消goroutine或goroutine定時取消等。context接口除了func WithCancel(parent Context) (ctx Context, cancel CancelFunc),還有衍生以下方法

  • func WithDeadline(parent Context, deadline time.Time) (Context, CancelFunc):
    此函數返回其父項的派生 context,當截止日期超過或取消函數被調用時,該 context 將被取消。例如,您可以創建一個將在以後的某個時間自動取消的 context,並在子函數中傳遞它。當因爲截止日期耗盡而取消該 context 時,獲此 context 的所有函數都會收到通知去停止運行並返回。
  • func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc):

    此函數類似於 context.WithDeadline。不同之處在於它將持續時間作爲參數輸入而不是時間對象。此函數返回派生 context,如果調用取消函數或超出超時持續時間,則會取消該派生 context。

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

    此函數接收 context 並返回派生 context,其中值 val 與 key 關聯,並通過 context 樹與 context 一起傳遞。這意味着一旦獲得帶有值的 context,從中派生的任何 context 都會獲得此值。不建議使用 context 值傳遞關鍵參數,而是函數應接收簽名中的那些值,使其顯式化。

有興趣的同學請閱讀:https://studygolang.com/pkgdoc

參考:

https://tutorialedge.net/gola...

http://goinbigdata.com/golang...

https://medium.com/code-zen/c...

https://blog.csdn.net/u013029...

links

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章