Go 語言通知協程退出 -取消- 的幾種方式

在 Go 語言中,控制 goroutine 的退出或取消很重要,這能使資源得到合理利用,避免潛在的內存泄露。

如下是一些在 Go 中通知協程退出的常見方式:

  1. 使用通道(Channel:通過發送特定的信號或關閉通道來通知協程退出。這是最簡單直接的方法。

  2. 使用 contextcontext 包提供了一種更標準化的方式來傳遞取消信號、超時、截止時間等控制信息。

  3. ** 使用 sync.WaitGroup**:雖然 WaitGroup 本身不用於發送取消信號,但它可以用來等待一組協程完成,通常與其他方法(如通道)結合使用來控制協程的退出。

1. 使用通道

package main

import (
    "fmt"
    "time"
)

func worker(exitChan chan bool) {
    for {
        select {
        case <-exitChan:
            fmt.Println("Worker exiting.")
            return
        default:
            fmt.Println("Working...")
            time.Sleep(1 * time.Second)
        }
    }
}

func main() {
    exitChan := make(chan bool)
    go worker(exitChan)

    time.Sleep(3 * time.Second) // 模擬做了一些工作
    exitChan <- true            // 通知協程退出
    time.Sleep(1 * time.Second) // 給協程時間退出
}

輸出:

Working...
Working...
Working...
Working...
Worker exiting.

在線代碼 [1]

2. 使用 context

package main

import (
    "context"
    "fmt"
    "time"
)

func worker(ctx context.Context) {
    for {
        select {
        case <-ctx.Done():
            fmt.Println("Worker exiting.")
            return
        default:
            fmt.Println("Working...")
            time.Sleep(1 * time.Second)
        }
    }
}

func main() {
    ctx, cancel := context.WithCancel(context.Background())
    go worker(ctx)

    time.Sleep(3 * time.Second)  // 模擬做了一些工作
    cancel()                     // 通知協程退出
    time.Sleep(1 * time.Second)  // 給協程時間退出
}

輸出:

Working...
Working...
Working...
Working...
Worker exiting.

在線代碼 [2]

在上面這兩個示例中,當主函數完成其工作後,通過通道發送信號或調用 cancel 函數來通知協程退出。使用 context 包是更推薦的做法,因爲其提供了一種更標準化和靈活的方式來管理協程的生命週期。

3. 使用 sync.WaitGroup 控制協程退出

sync.WaitGroup 主要用於等待一組協程的完成。其不直接提供通知協程退出的機制,但可以與其他方法(如通道)結合使用來控制協程的退出。

示例:

package main

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

func worker(id int, wg *sync.WaitGroup, stopCh chan struct{}) {
    defer wg.Done()
    for {
        select {
        case <-stopCh: // 接收退出信號
            fmt.Printf("Worker %d stopping\n", id)
            return
        default:
            fmt.Printf("Worker %d working\n", id)
            time.Sleep(time.Second)
        }
    }
}

func main() {
    var wg sync.WaitGroup
    stopCh := make(chan struct{})

    workerCount := 3
    wg.Add(workerCount)

    for i := 0; i < workerCount; i++ {
        go worker(i, &wg, stopCh)
    }

    time.Sleep(3 * time.Second) // 模擬工作
    close(stopCh)               // 發送退出信號給所有協程

    wg.Wait() // 等待所有協程退出
    fmt.Println("All workers stopped")
}

輸出:

Worker 0 working
Worker 2 working
Worker 1 working
Worker 2 working
Worker 0 working
Worker 1 working
Worker 0 working
Worker 2 working
Worker 1 working
Worker 2 stopping
Worker 0 stopping
Worker 1 stopping
All workers stopped

在線代碼 [3]

在上例中,stopCh 通道用於通知協程退出。當關閉 stopCh 時,所有監聽這個通道的協程都會接收到信號,並優雅地停止執行。

但我覺得這個例子並不好, 本質上成了和 **1. 使用通道(Channel)**一樣了..

sync.WaitGroup的真正作用是卡在wg.Wait()處,直到wg.Done()被執行 (wg.Add()增加的值被減爲 0), 纔會繼續往下執行. 比如往往用於防止 goroutine 還沒執行完, 主協程就退出了

另外, 如果是性能敏感場景, 往往使用原子操作(Atomic)在多個協程之間安全地共享狀態 (原子操作用於安全地讀寫共享狀態,可以用來設置一個標誌,協程可以定期檢查這個標誌來決定是否退出), 而不使用通道來做協程間的通信

更多參考:

golang context 父子任務同步取消信號 [4]

Go 程序員面試筆試寶典 - context 如何被取消 [5]

如何退出協程 goroutine (其他場景)[6]

go 協程取消 [7]

參考資料

[1]

在線代碼: https://go.dev/play/p/HrZbNO-jyKf

[2]

在線代碼: https://go.dev/play/p/hg_w1bxcmyg

[3]

在線代碼: https://go.dev/play/p/9AwV2v9iqdu

[4]

golang context 父子任務同步取消信號: https://blog.csdn.net/whatday/article/details/113771225

[5]

Go 程序員面試筆試寶典 - context 如何被取消: https://golang.design/go-questions/stdlib/context/cancel/

[6]

如何退出協程 goroutine (其他場景): https://geektutu.com/post/hpg-exit-goroutine.html

[7]

go 協程取消: https://www.google.com/search?q=go%E5%8D%8F%E7%A8%8B%E5%8F%96%E6%B6%88

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/CNTlXMWSmQ4dNbbCQpbjYg