「Golang成長之路」併發之channel篇2

yangkuang發表於2021-10-04

一、使用channel等待任務結束

在前面的內容中很多地方使用到了:
time.Sleep(time.Millisecond)
如:

func chanDemo(){
    var channels [10]chan int  //建立channel陣列
    for i := 0; i < 10; i++{
        channels[i] = creatworker(i)
    }
    for i := 0; i < 10; i++{
        channels[i] <- 'a' + 1
    }
    time.Sleep(time.Millisecond)
}
func bufferedChannel(){
    ch := make(chan int, 3)
    go worker(0, ch)
    for i := 0; i < 10; i++{
        ch <- 'a' + i
    }
    time.Sleep(time.Millisecond)
}
func channelClose(){
    ch := make(chan int)
    go worker(0, ch)
    ch <- 'a'
    ch <- 'b'
    ch <- 'c'
    ch <- 'd'
    close(ch)
    time.Sleep(time.Millisecond)
}
在這些方法裡面我們很容易知道他們執行所消耗的時間,但事實上,很多程式的時間是不能預估的,我們不能一直是用time包來對程式執行的時間進行預估,是不靠譜的,所以這裡我們有了“使用channel等待任務結束”

先看這段程式碼:

func chanDemo(){
   var channels [10]chan int //建立channel陣列
   for i := 0; i < 10; i++{
      channels[i] = creatworker(i)
   }
   for i := 0; i < 10; i++{
      channels[i] <- 'a' + 1
  }
   time.Sleep(time.Millisecond)
}

我們需要使用channel併發的列印10個字母,為了讓字母完整列印,我們對程式執行時間進行了預估,讓程式執行1毫秒就結束;下面我們需要使用
1.使用channel等待任務結束
仍然列印字母(列印20個):部分內容見程式碼註釋
使用’chan bool’的通道來共享通訊來使用記憶體,告訴main任務結束

package main

import (
   "fmt"
)
//定義一個結構體
//包含一個 'chan int ' 的in和 'chan bool'的done
type Worker struct{
   in chan int
   done chan bool  //對done的接和收作為結束的訊號
}

func DoWork( id int, c chan int, done chan bool){
   for {
      n := <-c //接受channel的內容
      fmt.Printf("worker %d received %c\n", id, n)
      done <- true  //done接收資料true
  }
}

func createWorker(id int) Worker {
   //建立Worker的一個物件w
   w := Worker{make(chan int),
   make(chan bool),
   }

   go DoWork(id, w.in, w.done) 此處啟動goroutine,即併發
   return w
}

func ChanDemo() {

   var workers [10]Worker  //建立10個抽象型別Worker

  for i := 0; i < 10; i++{ 
      workers[i] = createWorker(i) //建立10個Worker的物件,並返回給workers[i]
   }

   for i := 0; i < 10; i++{  //可使用range
      workers[i].in <- 'a' + i   //workers[i].in接受資料
   }

   for _, worker := range workers {
      <-worker.done   //將done的資料發給mian,告知main該任務結束
   }

   for i := 0; i < 10; i++{ //可使用range
      workers[i].in <- 'A'+ i  //workers[i].in接受資料
   }

   for _, worker := range workers{
      <- worker.done  //將done的資料發給mian,告知main該任務結束
   }
}

func main(){
   ChanDemo()
}

列印結果:
worker 0 received a
worker 5 received f
worker 1 received b
worker 6 received g
worker 4 received e
worker 9 received j
worker 8 received i
worker 2 received c
worker 7 received h
worker 3 received d
worker 6 received G
worker 2 received C
worker 3 received D
worker 7 received H
worker 1 received B
worker 4 received E
worker 5 received F
worker 0 received A
worker 9 received J
worker 8 received I

從列印結果看出:是按順序列印的(先小寫後大寫)
這裡還有一種方法:

func ChanDemo() {

   var workers [10]Worker

  for i := 0; i < 10; i++{
      workers[i] = createWorker(i)
   }
   for i := 0; i < 10; i++{
      workers[i].in <- 'a' + i
   }
   for i := 0; i < 10; i++{
      workers[i].in <- 'A'+ i
   }
   //將兩個<- worker.done 放在一起
   for _, worker := range workers{
      <- worker.done   
      <- worker.done
   }
}

但是需要注意的是:
我們一共建立了10個goroutine,在第二個for中就已經向所有channel中傳送資料,接著第三個for,又向channel中傳送資料,這樣會死鎖,因為第一次channel中的資料沒有人來接,然後又向channel發資料。
解決方法:在DoWork函式增加併發,讓done <- true處於併發執行狀態,可隨時向main發資料。

func DoWork( id int, c chan int, done chan bool){
   for {
      n := <-c //接受channel的內容
      fmt.Printf("worker %d received %c\n", id, n)
      go func() {
        done <- true
     }()
  }
}

2.使用系統提供的 ‘WaitGroup’等待任務結束
WaitGroup提供了:Add()、Wait()、Done()方法

package main

import (
   "fmt"
 "sync")

type Worker struct{
   in chan int
   wg *sync.WaitGroup  //引用需要指標
}

func DoWork( id int, c chan int, wg *sync.WaitGroup){
   for {
      n := <-c //接受channel的內容
      fmt.Printf("worker %d received %c\n", id, n)
       wg.Done()  //接和收結束資訊
   }
}

func createWorker(id int, wg *sync.WaitGroup) Worker {
   //建立Worker的一個物件w
   w := Worker{make(chan int), wg,}

   go DoWork(id, w.in, wg)
   return w
}

func ChanDemo() {
   var wg sync.WaitGroup

   var workers [10]Worker
   for i := 0; i < 10; i++{
      workers[i] = createWorker(i, &wg)  //指標
   }
   wg.Add(20)  //20個任務
   for i := 0; i < 10; i++{
      workers[i].in <- 'a' + i
   }
   for i := 0; i < 10; i++{
      workers[i].in <- 'A'+ i
   }
   wg.Wait()  //任務結束
}

func main(){
   ChanDemo()
}

列印結果:
worker 0 received a
worker 4 received e
worker 6 received g
worker 2 received c
worker 9 received j
worker 7 received h
worker 0 received A
worker 8 received i
worker 5 received f
worker 3 received d
worker 1 received b
worker 1 received B
worker 9 received J
worker 2 received C
worker 3 received D
worker 4 received E
worker 7 received H
worker 8 received I
worker 6 received G
worker 5 received F

二、在這裡總結了幾個常見的問題:

func ChanDemo() {

var workers [10]Worker

  for i := 0; i < 10; i++{
workers[i] = createWorker(i)
}

for i := 0; i < 10; i++{
workers[i].in <- 'a' + i
}

在第一個for中,第一步workers[0] = createWorker(0)

然後就進入這裡

func createWorker(id int) Worker {
//建立Worker的一個物件w
  w := Worker{make(chan int),
     make(chan bool),
     }

go DoWork(id, w.in, w.done)
return w
}

在這個函式中我們開了一個goroutine,同時我們會將w返回給workers[0],然後就進入:

for i := 0; i < 10; i++{
workers[i] = createWorker(i)
}

的第二次,第三次迴圈……

直到迴圈結束。

但是這裡就有問題了,在這個途中我們一共開了10 goroutine,但是這10 goroutine都處於等待狀態(因為我們還沒有給channel任何內容,從我們的輸出結果可以看出)

  1. 那麼這裡的10個goroutine是處於等待狀態是不是因為,我們channel沒有接受到任何資訊,所以就會造成goroutine的等待?

2. 還有這裡:

 func DoWork( id int, c chan int, done chan bool){
    for {
         n := <-c //接受channel的內容
         fmt.Printf("id: %v, chan:%c\n", id, n)
         done <- true
      }
    }

這個死迴圈,為什麼在函式呼叫後只迴圈了一遍? 當然這裡我知道他是其中一個goroutine

3. 當然還有一個問題,就是我們在前兩個問題在基礎上,呼叫函式DoWork()時,也會對應的將true傳送給與之對應的workers[i].done中,然後:

for i := 0; i < 10; i++{
workers[i].in <- 'a' + i
}

for _, worker := range workers {
<- worker.done
}

在這裡的第二個for中,這裡<- worker.done全為true,我們是不是從這裡就可以瞭解到前面的10個goroutine結束了?

4. 也正是因為這樣我們才不需要time包,來預計程式的執行時間了?

三、回答
  1. 是的,它們此時都在等待,等別人從in中傳送任務資料。

  2. 這是個死迴圈,一般我們goroutine中常會這麼寫,只要有任務就做。視訊裡實際上大寫字母,小寫字母,一共執行兩遍。執行多少遍取決於外界,這裡是main函式,到底傳送了多少任務給我這個worker[i]。

  3. 這裡的true方向同學搞錯了,是worker通知main函式,說我做完了。<- worker.done這裡是main函式接收worker.done的資料,如果收到,就說明這個worker的事情做完了。

  4. ​是的,理想情況下應該不需要time包來預計執行時間。預計的時間會不靠譜。

本作品採用《CC 協議》,轉載必須註明作者和本文連結

相關文章