👨‍💻簡介

昨天講到Goroutine的橋梁aka傳話筒 - Channel,那要怎麼知道對方有收到訊息,我的紙條有送到對方手上呢? 今天就是要來介紹幾種Goroutine的確定完成工作的幾種方式。

Goroutine 的等待方式

在Go語言中,等待Goroutine完成的方式有多種。

sync.WaitGroup

sync.WaitGroup是Go語言中常見的一種等待方式,它允許我們等待一組Goroutine完成。

package main

import (
    "fmt"
    "sync"
)

func worker(id int, wg *sync.WaitGroup) {
    defer wg.Done()                        // 表示Goroutine完成工作,減少WaitGroup計數
    fmt.Printf("Worker %d 正在工作\n", id)
}

func main() {
    var wg sync.WaitGroup
    for i := 1; i <= 3; i++ {
        wg.Add(1)                         // 增加WaitGroup數量
        go worker(i, &wg)                 // 啟動Goroutine,並將WaitGroup的指針傳遞給它
    }
    wg.Wait()                             // 將主程式阻塞,直到所有Goroutines完成工作。
    fmt.Println("所有工作完成")
}

在上面的範例中,我們先創建了一個sync.WaitGroup的變數wg,並使用for循環去執行我們的goroutine,每次執行前都先將等待goroutine的數量+1,總共等待三個Goroutine完成工作。 接著使用 wg.Wait()將main函數先暫停一下,等所有goroutine都完成工作,wg的計數器為0,在繼續執行程式。 在 worker函數裡,會接收 wg的pointer,並在開頭使用defer wg.Done()在程式執行結束就將等待的goroutine數量-1,來確保goroutine會完成

Channel

Channel是Go語言中用於通信的機制,也可以用於等待Goroutine完成。

package main

import (
    "fmt"
)

func worker(id int, ch chan bool) {
    fmt.Printf("Worker %d 正在工作\n", id)
    ch <- true
}

func main() {
    ch := make(chan bool)
    for i := 1; i <= 3; i++ {
        go worker(i, ch)
    }
    for i := 1; i <= 3; i++ {
        <-ch
    }
    fmt.Println("所有工作完成")
}

Channel的特性就是有方向性,在這段程式碼中,創建了三個Goroutines,每個Goroutine在執行時都印出一條訊息,然後將一個布林值傳送到Channel中,最後等三個Channel都接收到操作完成後,印出 “所有工作完成”。

select

使用select語句可以等待多個Goroutine中的任何一個完成。

package main

import (
    "fmt"
    "time"
)

func worker(id int, ch chan bool) {
    fmt.Printf("Worker %d 正在工作\n", id)
    time.Sleep(time.Second * time.Duration(id))
    ch <- true
}

func main() {
    ch1 := make(chan bool)
    ch2 := make(chan bool)

    go worker(1, ch1)
    go worker(2, ch2)

    select {
    case <-ch1:
        fmt.Println("Worker 1 完成")
    case <-ch2:
        fmt.Println("Worker 2 完成")
    }

    fmt.Println("工作完成")
}

在這個範例中,我們啟動了兩個Goroutines(worker(1, ch1)worker(2, ch2)),它們各自執行一些工作並將布林值寫入Channel。接著,我們使用 select 來等待哪個Channel中的值會先到達,即哪個Goroutine先完成工作。

select的特性, 只會選擇其中一個 case,如果兩個Goroutines都完成,只會選擇一個來處理,而不會等待所有Goroutines完成。這也使得 select 非常適合用來需要競爭的情況,可以根據需要增加更多的 case 來等待多個Goroutines。

time.Sleep

time.Sleep也是一種簡單的等待方式,但它只是預測多久完成,而且會阻塞整個執行緒,不推。

package main

import (
    "fmt"
    "time"
)

func worker(id int) {
    fmt.Printf("Worker %d 正在工作\n", id)
    time.Sleep(time.Second * time.Duration(id))
    fmt.Printf("Worker %d 完成\n", id)
}

func main() {
    go worker(1)
    go worker(2)

    // 等待一段時間以確保工作完成
    time.Sleep(time.Second * 3)

    fmt.Println("所有工作完成")
}

Goroutine 等待方式的比較

不同的等待方式有各自的使用場景和優缺點。sync.WaitGroup是最常用的方式,因為它簡單且高效。Channel等待方式在需要更多控制時很有用,而select則適用於等待多個Goroutine中的某一個。請根據您的需求選擇最適合的方式。

當你在Go語言中使用Goroutines並需要等待它們完成時,有幾種不同的等待方式可供選擇。以下是這些等待方式的比較:

  1. sync.WaitGroup

    • 優點

      • 簡單易用
      • 效率高:等待期間不會浪費CPU資源。
      • 可用於任意數量的Goroutines。
    • 限制

      • 需要額外的記憶體:每個等待的Goroutine都需要一個WaitGroup。
      • 只能等待固定數量的Goroutines。
  2. Channel(Channel)等待

    • 優點

      • 可以靈活控制等待:你可以在Goroutine完成時向Channel發送信號。
      • 可以用於不定數量的Goroutines。
      • 可以用於任何需要自定義等待邏輯的情況。
    • 限制

      • 需要額外的程式碼來處理Channel。
      • 可能需要額外的同步以確保所有Channel操作正確。
  3. select 多Channel選擇

    • 優點

      • 可以等待多個Goroutines中的任何一個完成。
      • 適用於需要競爭的情況。
    • 限制

      • 複雜性:當你有多個Channel時,可能需要更多的程式碼和邏輯。
      • 只能等待一個完成,無法等待多個Goroutines全部完成。
  4. time.Sleep

    • 優點

      • 簡單明瞭,容易理解。
    • 限制

      • 不是一種有效的等待方式,因為會阻塞整個函數,浪費CPU資源。
      • 無法確保Goroutines何時完成。
      • 不推薦在正式環境中使用。

這樣看下來,最常見且推薦的方式就是使用sync.WaitGroup,因為簡單而且高效。但根據不同情況,你可能需要使用其他方式。使用Channel等待和select適用於需要更多控制和競爭的情況,而time.Sleep只應該在測試或學習目的中使用。選擇適合你需求的等待方式,才能確保Goroutines的並發能夠在你的應用中正確運作。

小結

前兩篇介紹了Goroutine、Channel,最後就來介紹一下如何確保你的工作有完成,紙條有成功送到對方手上。了解如何等待它們完成是開發高效併發應用的重要一環。介紹了多種等待Goroutine的方式,每種方式都有他的優勢和限制。可以根據需求,選擇適合的方式來讓你程式碼能夠充分利用Goroutine的優點。

📚Reference