Go 協程與通道

在談論 goroutines 時,我們需要記住 Go 語言的簡潔性,並且它非常強調併發處理。併發是指能夠獨立地處理多個任務,一項接一項地進行,這與並行不同,後者是指任務同時執行。我在之前的文章中已經討論過併發和並行的區別,所以這裏不再深入探討。

Goroutines 的基本概念

讓我們直接進入 Go 中 goroutines 的主題,這是在代碼中實現併發處理的一種方式。它們非常適合在提高性能的同時不犧牲數據安全性和完整性的場景。這就是 goroutines 和 channels 結合使用的意義所在。

在這篇文章中,我們將更側重於技術層面,通過實際代碼示例來進行探索,而不是理論解釋。

簡而言之,當我們在 Go 中使用關鍵字 go 時,我們是在指示某個函數的執行應該是併發的。例如,如果我們有三個互不依賴的函數,我們可以同時執行它們,而不是等待一個完成後再開始下一個。

示例代碼

以下是一個使用 goroutines 同時執行三個操作的示例:

package main

import (
    "fmt"
    "math/rand"
    "sync"
    "time"
)

// 模擬一個耗時的計算
func performCalculation(id int, wg *sync.WaitGroup) {
    defer wg.Done() // 表示 goroutine 完成
    fmt.Printf("Goroutine %d: 開始計算...\n", id)
    time.Sleep(time.Duration(rand.Intn(3)) * time.Second) // 模擬處理時間
    fmt.Printf("Goroutine %d: 計算完成。\n", id)
}

// 模擬讀取外部 API
func fetchData(id int, wg *sync.WaitGroup) {
    defer wg.Done() // 表示 goroutine 完成
    fmt.Printf("Goroutine %d: 開始讀取 API...\n", id)
    time.Sleep(time.Duration(rand.Intn(3)) * time.Second) // 模擬 API 響應時間
    fmt.Printf("Goroutine %d: 接收到 API 數據。\n", id)
}

// 模擬日誌記錄
func writeLog(id int, wg *sync.WaitGroup) {
    defer wg.Done() // 表示 goroutine 完成
    fmt.Printf("Goroutine %d: 開始記錄日誌...\n", id)
    time.Sleep(time.Duration(rand.Intn(3)) * time.Second) // 模擬寫入時間
    fmt.Printf("Goroutine %d: 日誌記錄完成。\n", id)
}

func main() {
    rand.Seed(time.Now().UnixNano()) // 確保隨機時間
    var wg sync.WaitGroup // 創建一個 WaitGroup

    // 增加 WaitGroup 計數器
    wg.Add(3)

    // 啓動三個併發的 goroutines
    go performCalculation(1, &wg)
    go fetchData(2, &wg)
    go writeLog(3, &wg)

    // 等待所有 goroutines 完成
    wg.Wait()
    fmt.Println("所有任務已完成。")
}

代碼解析

main 函數中,我們首先創建了一個 WaitGroup 變量 wg,用於控制和同步 goroutines。接着,我們使用 wg.Add(3) 來設置我們將控制的 goroutines 數量。使用 go 關鍵字啓動 goroutines,以併發方式執行。

這三個函數只是模擬了可以併發執行的場景,使用 Done 方法和 defer 來指示 goroutine 的完成狀態。從上面的輸出可以看出,三個 goroutines 是併發執行的,這種方式比逐個調用函數並等待其完成更高效。

使用 Channels 進行通信

然而,在某些情況下,我們需要在保持代碼性能和數據完整性的同時處理相互依賴的函數。我們可以創建兩個 goroutines 並在它們之間使用 channel。以下是一個示例:

package main

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

func process(ctx context.Context, ch chan int, wg *sync.WaitGroup) {
    defer wg.Done()
    for i := 0; i < 10; i++ {
        select {
        case <-ctx.Done():
            fmt.Println("process canceled")
            return
        case ch <- i:
            fmt.Println("sending", i)
            time.Sleep(500 * time.Millisecond)
        }
    }
    close(ch)
}

func consumer(ch chan int, wg *sync.WaitGroup) {
    defer wg.Done()
    for v := range ch {
        fmt.Println("received", v)
        time.Sleep(1 * time.Second)
    }
}

func main() {
    ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
    defer cancel()

    ch := make(chan int)
    wg := sync.WaitGroup{}
    wg.Add(2)

    go process(ctx, ch, &wg)
    go consumer(ch, &wg)

    wg.Wait()
}

代碼解析

main 函數中,我們首先創建了一個上下文 ctx,用於演示 context.WithTimeout 的使用,它允許我們定義操作的最長持續時間。在函數執行結束時,通過 defer 確保調用 cancel 方法以釋放與上下文關聯的資源。

我們還創建了一個 channel,並使用 make 關鍵字指定它是一個通道,並推斷其傳輸的數據類型。與前面的示例一樣,我們使用 WaitGroup 控制 goroutines 的執行流,確保主程序在所有 goroutines 完成之前不會繼續執行。

接下來,我們聲明瞭 process 函數,負責處理數據。該函數接收三個參數:上下文(ctx)、WaitGroupwg)和通道(ch)。在 process 函數中,有一個 for 循環迭代直到 i 小於 10。然而,我們使用了一個 select 語句來處理兩個 case

  1. 當上下文到期(達到 main 函數中定義的 5 秒超時時間)時,執行中斷。

  2. 當處理完成時,處理過的值被髮送到通道。

之後,我們使用 consumer 函數,遍歷通道中的值,模擬隊列行爲。這樣,我們在兩個相互依賴的函數之間創建了一個併發流:一個生產數據,另一個消費數據。

總結

總之,儘可能分析你的代碼並識別可以進行性能改進的區域。使用 goroutines 進行併發處理,並通過 channels 在它們之間進行通信,可以顯著提高處理性能,同時確保數據的完整性和安全性。

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