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
)、WaitGroup
(wg
)和通道(ch
)。在 process
函數中,有一個 for
循環迭代直到 i
小於 10。然而,我們使用了一個 select
語句來處理兩個 case
:
-
當上下文到期(達到
main
函數中定義的 5 秒超時時間)時,執行中斷。 -
當處理完成時,處理過的值被髮送到通道。
之後,我們使用 consumer
函數,遍歷通道中的值,模擬隊列行爲。這樣,我們在兩個相互依賴的函數之間創建了一個併發流:一個生產數據,另一個消費數據。
總結
總之,儘可能分析你的代碼並識別可以進行性能改進的區域。使用 goroutines 進行併發處理,並通過 channels 在它們之間進行通信,可以顯著提高處理性能,同時確保數據的完整性和安全性。
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/f73nKYV2FSQ4IUCR_VX0CA