goroutine併發控制

windzhu0514發表於2019-03-23

通訊

共享記憶體

func  Test() {
    ordersInfoApp  :=  make([]orderInfoApp, 0, totalCount)
    var  mux sync.Mutex
    wg  := sync.WaitGroup{}

    for  i  :=  0; i <=  10; i++ {
        wg.Add(1)
        go  func(pageIndex int) {
            // do somethine
            var  ordersInfo orderInfoApp
            mux.Lock()
            ordersInfoApp  =  append(ordersInfoApp, ordersInfo)
            mux.Unlock()

            wg.Done()
        }(i)
    }

    wg.Wait()
}

一般在簡單的資料傳遞下使用

channel

func  Test() {
    ordersInfoApp  :=  make([]orderInfoApp, 0, totalCount)
    choi  :=  make(chan orderInfoApp, 10)
    wg  := sync.WaitGroup{}

    for  i  :=  0; i <=  10; i++ {
        wg.Add(1)
        go  func(pageIndex int) {
            // do somethine
            var  ordersInfo orderInfoApp
            choi <- ordersInfo

            wg.Done()
        }(i)
    }

    go  func() {
        wg.Wait()
        close(choi)
    }()

    for  v  :=  range choi {
        ordersInfoApp  =  append(ordersInfoApp, v)
    }
}

相對複雜的資料流動情況

同步和控制

goroutine 退出只能由本身控制,不能從外部強制結束該 goroutine 兩種例外情況:main 函式結束或者程式崩潰結束執行

共享變數控制結束

func  main() {
    running  :=  true
    f  :=  func() {
        for running {
            fmt.Println("i am running")
            time.Sleep(1  * time.Second)
        }
        fmt.Println("exit")
    }
    go  f()
    go  f()
    go  f()
    time.Sleep(2  * time.Second)
    running  =  false
    time.Sleep(3  * time.Second)
    fmt.Println("main exit")
}

優點: 實現簡單,不抽象,方便,一個變數即可簡單控制子 goroutine 的進行。 缺點: 結構只能是多讀一寫,不能適應結構複雜的設計,如果有多寫,會出現資料同步問題,需要加鎖或者使用 sync.atomic 不適合用於同級的子 go 程間的通訊,全域性變數傳遞的資訊太少 因為是單向通知,主程式無法等待所有子 goroutine 退出 這種方法只適用於非常簡單的邏輯且併發量不太大的場景

sync.Waitgroup 等待結束

func  main() {
    var  wg sync.WaitGroup
    for  i  :=  0; i <  3; i++ {
        wg.Add(1)
        go  func() {
            defer wg.Done()
            // do something
        }()
    }

    wg.Wait()
}

channel 控制結束

// 可擴充套件到多個work
func  main() {
    chClose  :=  make(chan  struct{})
    go  func() {
        for {
            select {
                case  <-chClose:
                    return
                default:
            }

        // do something
        }
    }()

    //chClose<-struct{}
    close(chClose)
}

注意 channel 的阻塞情況,避免出現死鎖。 通常 channel 只能由傳送者關閉

  • 向無緩衝的 channel 寫入資料會導致該 goroutine 阻塞,直到其他 goroutine 從這個 channel 中讀取資料

  • 從無緩衝的 channel 讀出資料,如果 channel 中無資料,會導致該 goroutine 阻塞,直到其他 goroutine 向這個 channel 中寫入資料

  • 向帶緩衝的且緩衝已滿的 channel 寫入資料會導致該 goroutine 阻塞,直到其他 goroutine 從這個 channel 中讀取資料

  • 向帶緩衝的且緩衝未滿的 channel 寫入資料不會導致該 goroutine 阻塞

  • 從帶緩衝的 channel 讀出資料,如果 channel 中無資料,會導致該 goroutine 阻塞,直到其他 goroutine 向這個 channel 中寫入資料

  • 從帶緩衝的 channel 讀出資料,如果 channel 中有資料,該 goroutine 不會阻塞

    // 讀完結束
    for {
    select {
    case  <-ch:
        default:
        goto finish
    }
    }
    finish:
  • 如果多個 case 同時就緒時,select 會隨機地選擇一個執行

  • case 標籤裡向 channel 傳送或接收資料,case 後面的語句在傳送接收成功後才會執行
  • nil channel(讀、寫、讀寫)的 case 標籤會被跳過

limitwaitgroup

type  limitwaitgroup  struct {
    sem chan  struct{}
    wg sync.WaitGroup
}

func  New(n int) *limitwaitgroup {
    return  &limitwaitgroup{
        sem: make(chan  struct{}, n),
    }
}

func  (l *limitwaitgroup) Add() {
    l.sem <-  struct{}{}
    l.wg.Add(1)
}

func  (l *limitwaitgroup) Done() {
    <-l.sem
    l.wg.Done()
}

func  (l *limitwaitgroup) Wait() {
    l.wg.Wait()
}

// 例子
wg  := limitwaitgroup.New(6)
for  i  :=  0; i <=  10; i++ {
    wg.Add()
    go  func(index int){
        defer wg.Done()
        // do something
    }(i)
}
wg.Wait()

context

上下文 go 1.7 作為第一個引數在 goroutine 裡傳遞

Context 的介面定義

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

Deadline獲取設定的截止時間(WithDeadline、WithTimeout), 第一個返回值代表截止時間,第二個返回值代表是否設定了截止時間,false 時表示沒有設定截止時間

Done方法返回一個關閉的只讀的 chan,型別為struct{},在 goroutine 中,如果該方法返回的 chan 可以讀取,則意味著 parent context 已經發起了取消請求,我們通過Done方法收到這個訊號後,就應該做清理操作,然後退出 goroutine,釋放資源。

Errcontext 沒有被結束,返回 nil;已被結束,返回結束的原因(被取消、超時)。

Value方法通過一個 Key 獲取該 Context 上繫結的值,訪問這個值是執行緒安全的。key 一般定義當前包的一個新的未匯出型別的變數(最好不要使用內建型別),避免和其他 goroutine 的 key 衝突。

  • 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

這四個With函式,接收的都有一個 partent 引數,就是父 Context,我們要基於這個父 Context 建立出子 Context 的意思,這種方式可以理解為子 Context 對父 Context 的繼承,也可以理解為基於父 Context 的衍生。

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

WithCancel函式,傳遞一個父 Context 作為引數,返回子 Context,以及一個取消函式用來取消 Context。 WithDeadline函式,和WithCancel差不多,它會多傳遞一個截止時間引數,意味著到了這個時間點,會自動取消 Context,也可以不等到這個時候,可以提前通過取消函式進行取消。

WithTimeoutWithDeadline基本上一樣,這個表示是超時自動取消,設定多少時間後自動取消 Context。

WithValue函式和取消 Context 無關,生成一個繫結了一個鍵值對資料的 Context,這個繫結的資料可以通過Context.Value方法訪問到

  • 例子

WithCancel

func  main() {
    ctx, cancel  := context.WithCancel(context.Background())
    go  watch(ctx, "goroutine 1")
    go  watch(ctx, "goroutine 2")
    go  watch(ctx, "goroutine 3")
    time.Sleep(10  * time.Second)
    fmt.Println("開始結束goroutine")
    cancel()
    time.Sleep(5  * time.Second)
    fmt.Println(ctx.Err())
}
func  watch(ctx context.Context, name string) {
  for {
      select {
      case  <-ctx.Done():
          fmt.Println(name, "over")
          return
      default:
          fmt.Println(name, "running")
          time.Sleep(2  * time.Second)
      }
  }
}

// output:
goroutine 1 running
goroutine 2 running
goroutine 3 running
goroutine 1 running
goroutine 2 running
goroutine 3 running
goroutine 1 running
goroutine 2 running
goroutine 3 running
goroutine 2 running
goroutine 3 running
goroutine 1 running
goroutine 3 running
goroutine 2 running
goroutine 1 running
開始結束goroutine
goroutine 1 over
goroutine 2 over
goroutine 3 over
context canceled

WithDeadline

func  main() {
    ctx, cancel  := context.WithDeadline(context.Background(), time.Now().Add(5*time.Second))
    go  watch(ctx, "goroutine 1")
    go  watch(ctx, "goroutine 2")
    go  watch(ctx, "goroutine 3")
    _  = cancel
    time.Sleep(8  * time.Second)
    fmt.Println(ctx.Err())
}

func  watch(ctx context.Context, name string) {
  for {
      select {
      case  <-ctx.Done():
          fmt.Println(name, "over")
          return
      default:
          fmt.Println(name, "running")
          time.Sleep(2  * time.Second)
      }
  }
}

// output:
goroutine 3 running
goroutine 2 running
goroutine 1 running
goroutine 3 running
goroutine 1 running
goroutine 2 running
goroutine 1 running
goroutine 3 running
goroutine 2 running
goroutine 3 over
goroutine 1 over
goroutine 2 over
context deadline exceeded

WithTimeout

func  main() {
    ctx, cancel  := context.WithTimeout(context.Background(), 5*time.Second)
    go  watch(ctx, "goroutine 1")
    go  watch(ctx, "goroutine 2")
    go  watch(ctx, "goroutine 3")
    _  = cancel
    time.Sleep(8  * time.Second)
    fmt.Println(ctx.Err())
}

func  watch(ctx context.Context, name string) {
  for {
      select {
      case  <-ctx.Done():
          fmt.Println(name, "over")
          return
      default:
          fmt.Println(name, "running")
          time.Sleep(2  * time.Second)
      }
  }
}

// output:
goroutine 3 running
goroutine 1 running
goroutine 2 running
goroutine 3 running
goroutine 2 running
goroutine 1 running
goroutine 2 running
goroutine 1 running
goroutine 3 running
goroutine 2 over
goroutine 3 over
goroutine 1 over
context deadline exceeded

WithValue

type  key  int  // 未匯出的包私有型別
var  kkk key =  0

func  main() {
    ctx, cancel  := context.WithCancel(context.Background())
    // WithValue是沒有取消函式的
    ctx  = context.WithValue(ctx, kkk, "100W")

    go  watch(ctx, "goroutine 1")
    go  watch(ctx, "goroutine 2")
    go  watch(ctx, "goroutine 3")

    time.Sleep(8  * time.Second)

    fmt.Println("開始結束goroutine")
    cancel()

    time.Sleep(3  * time.Second)
    fmt.Println(ctx.Err())
}

func  watch(ctx context.Context, name string) {
  for {
      select {
      case  <-ctx.Done():
          fmt.Println(name, "over")
          return
      default:
          fmt.Println(name, "running", "爸爸給我了", ctx.Value(kkk).(string))
          time.Sleep(2  * time.Second)
      }
  }
}
// output:
goroutine 1 running 爸爸給我了 100W
goroutine 2 running 爸爸給我了 100W
goroutine 3 running 爸爸給我了 100W
goroutine 2 running 爸爸給我了 100W
goroutine 1 running 爸爸給我了 100W
goroutine 3 running 爸爸給我了 100W
goroutine 1 running 爸爸給我了 100W
goroutine 2 running 爸爸給我了 100W
goroutine 3 running 爸爸給我了 100W
goroutine 1 running 爸爸給我了 100W
goroutine 3 running 爸爸給我了 100W
goroutine 2 running 爸爸給我了 100W
開始結束goroutine
goroutine 2 over
goroutine 3 over
goroutine 1 running 爸爸給我了 100W
goroutine 1 over
context canceled

控制多個 goroutine

func  main() {
    http.HandleFunc("/", func(W http.ResponseWriter, r *http.Request) {
    fmt.Println("收到請求")

    ctx, cancel  := context.WithCancel(context.Background())
    go  worker(ctx, 2)
    go  worker(ctx, 3)

    time.Sleep(time.Second *  10)
    cancel()
    fmt.Println(ctx.Err())
    })
    http.ListenAndServe(":9290", nil)
}
func  worker(ctx context.Context, speed int) {
    reader  :=  func(n int) {
        for {
            select {
                case  <-ctx.Done():
                return
                default:
                break
            }
            fmt.Println("reader:", n)
            time.Sleep(time.Duration(n) * time.Second)
        }
    }

    go  reader(2)
    go  reader(1)

    for {
        select {
            case  <-ctx.Done():
            return
            default:
            break
        }
        fmt.Println("worker:", speed)
        time.Sleep(time.Duration(speed) * time.Second)
    }
}

// output:
收到請求
worker: 2
reader: 1
worker: 3
reader: 1
reader: 2
reader: 2
reader: 1
reader: 1
reader: 1
reader: 2
worker: 2
reader: 2
reader: 1
reader: 1
reader: 1
worker: 3
reader: 1
worker: 2
reader: 2
reader: 1
reader: 2
reader: 1
context canceled
  • 使用規則 使用 Context 的程式應遵循以下這些規則來保持跨包介面的一致和方便靜態分析工具(go vet)來檢查上下文傳播是否有潛在問題。

    • 不要將 Context 儲存在結構型別中,而是顯式的傳遞給每個需要的函式; Context 應該作為函式的第一個引數傳遞,通常命名為 ctx:
        func  DoSomething(ctx context.Context, arg Arg) error {
        // ... use ctx ...
    }
    • 即使函式可以接受 nil 值,也不要傳遞 nil Context。如果不確定要使用哪個 Context,請傳遞 context.TODO。

    • 使用 context 的 Value 相關方法只應該用於在程式和介面中傳遞和請求相關的後設資料,不要用它來傳遞一些可選的引數

    • 相同的 Context 可以傳遞給在不同 goroutine 中執行的函式; Context 對於多個 goroutine 同時使用是安全的。

來源: https://ljc.space/post/go-goroutine-concurrent-control/

相關文章