在現代軟體開發中,並發與非同步操作已成為不可或缺的一部分。Go 語言以其輕量級的 goroutine 和強大的並發處理能力,成為開發者進行並發編程的首選語言。然而,隨著程序的複雜度增加,如何有效地管理和協調多個 goroutine 的執行變得尤為重要。本文將針對初階 Go 開發者,介紹 Go 語言中 context
和 sync
套件在非同步操作中的應用,重點講解常用方法及其應用場景,並提供簡單易懂的代碼範例。
在程式設計中,同步(Synchronous)和非同步(Asynchronous)是兩種基本的執行模型。
同步操作:在執行一個同步操作時,程式會等待該操作完成後才繼續執行後續的代碼。例如,當一個函數被調用時,調用者會等待該函數返回結果後才繼續執行。
非同步操作:非同步操作允許程式在等待某個操作完成的同時,繼續執行其他代碼。這在處理 I/O 操作、網路請求等需要等待的任務時特別有用,能夠提升應用程序的效率和響應速度。
在 Go 語言中,goroutine 提供了一種輕量級的方式來實現非同步操作,但隨之而來的是多協程之間的協調和管理問題。這時,context
和 sync
套件就顯得尤為重要。
當應用程序中存在多個 goroutine 時,控制它們的執行變得非常重要。主要原因包括:
資源管理:限制同時運行的 goroutine 數量,防止過多的 goroutine 佔用過多資源,導致系統性能下降。
任務協調:確保多個 goroutine 按照預期的順序執行,避免競爭條件(Race Condition)和死鎖(Deadlock)等問題。
取消和超時控制:在需要的時候,可以取消不再需要的 goroutine 或設定操作的超時,提升應用的穩定性。
接下來,我們將介紹 context
和 sync
套件,這兩個套件在控制和協調多 goroutine 的執行方面扮演著重要角色。
context
套件提供了上下文控制的功能,允許我們在不同的 goroutine 之間傳遞取消信號、超時控制和附加值。主要用於控制一組相關的 goroutine,例如在處理 HTTP 請求時,當客戶端取消請求,所有與該請求相關的 goroutine 都應該被取消。
下面是一個使用 context
控制 goroutine 的簡單範例。該範例中,我們創建了一個可以取消的上下文,並在另一個 goroutine 中監聽取消信號,以便在需要時取消操作。
package main
import (
"context"
"fmt"
"time"
)
func main() {
// 創建一個可取消的上下文
ctx, cancel := context.WithCancel(context.Background())
// 啟動一個 goroutine,模擬長時間運行的操作
go func() {
for {
select {
case <-ctx.Done():
fmt.Println("Goroutine 被取消")
return
default:
fmt.Println("Goroutine 正在運行")
time.Sleep(500 * time.Millisecond)
}
}
}()
// 主程式等待 2 秒後取消上下文
time.Sleep(2 * time.Second)
cancel()
// 等待 goroutine 完成
time.Sleep(1 * time.Second)
fmt.Println("主程式結束")
}
說明:
context.WithCancel
創建了一個可取消的上下文 ctx
,並返回一個取消函數 cancel
。select
語句會監聽 ctx.Done()
,當上下文被取消時,會接收到取消信號並結束 goroutine。cancel()
,取消上下文,從而停止 goroutine 的運行。context.Background()
:返回一個空的上下文,通常作為所有上下文的根。context.WithCancel(parent)
:基於父上下文創建一個可取消的上下文。context.WithTimeout(parent, timeout)
:基於父上下文創建一個具有超時的上下文,超時後自動取消。context.WithValue(parent, key, value)
:基於父上下文創建一個帶有鍵值對的上下文,用於在 goroutine 之間傳遞值。context.WithTimeout
可以在指定的時間內自動取消上下文,非常適合用於需要設置操作超時的場景。
package main
import (
"context"
"fmt"
"time"
)
func main() {
// 設定一個 1 秒的超時上下文
ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second)
defer cancel()
// 啟動一個 goroutine,模擬長時間運行的操作
go func() {
select {
case <-time.After(2 * time.Second):
fmt.Println("Goroutine 完成工作")
case <-ctx.Done():
fmt.Println("Goroutine 被取消:", ctx.Err())
}
}()
// 等待 goroutine 完成或超時
time.Sleep(3 * time.Second)
fmt.Println("主程式結束")
}
說明:
context.WithTimeout
創建了一個在 1 秒後自動取消的上下文。select
會等待 2 秒後完成工作,但由於上下文在 1 秒後被取消,會先接收到取消信號並終止操作。sync
套件提供了多種同步原語,用於在多 goroutine 之間協調操作,確保數據的一致性和正確性。常用的同步工具包括互斥鎖(Mutex)、等待組(WaitGroup)、條件變量(Cond)等。
sync.WaitGroup
用於等待一組 goroutine 完成。這在需要等待多個並行操作完成後再繼續執行下一步時非常有用。
下面是一個使用 WaitGroup
的簡單範例,演示如何等待多個 goroutine 完成:
package main
import (
"fmt"
"sync"
"time"
)
func worker(id int, wg *sync.WaitGroup) {
defer wg.Done()
fmt.Printf("Worker %d 開始工作\n", id)
time.Sleep(time.Second)
fmt.Printf("Worker %d 完成工作\n", id)
}
func main() {
var wg sync.WaitGroup
// 啟動 3 個 goroutine
for i := 1; i <= 3; i++ {
wg.Add(1)
go worker(i, &wg)
}
// 等待所有 goroutine 完成
wg.Wait()
fmt.Println("所有工作完成")
}
說明:
WaitGroup
變量 wg
。wg.Add(1)
,表示有一個 goroutine 需要等待。wg.Done()
,表示該 goroutine 已完成。wg.Wait()
等待所有 goroutine 完成後再繼續執行。sync.Mutex
用於保護共享資源,防止多個 goroutine 同時訪問導致的數據競爭。
以下是一個使用 Mutex
保護共享變量的範例:
package main
import (
"fmt"
"sync"
)
func main() {
var (
mu sync.Mutex
count int
wg sync.WaitGroup
)
// 啟動 1000 個 goroutine 增加 count
for i := 0; i < 1000; i++ {
wg.Add(1)
go func() {
defer wg.Done()
mu.Lock()
count++
mu.Unlock()
}()
}
wg.Wait()
fmt.Printf("最終 count 的值為 %d\n", count)
}
說明:
Mutex
變量 mu
,用於保護共享變量 count
。count
前先調用 mu.Lock()
鎖定,操作完成後調用 mu.Unlock()
解鎖。WaitGroup
等待所有 goroutine 完成,確保 count
的最終值正確。sync.RWMutex
是一種讀寫互斥鎖,允許多個讀操作同時進行,但寫操作需要獨佔鎖。
package main
import (
"fmt"
"sync"
"time"
)
type SafeMap struct {
mu sync.RWMutex
m map[string]int
}
func (s *SafeMap) Read(key string) (int, bool) {
s.mu.RLock()
defer s.mu.RUnlock()
val, ok := s.m[key]
return val, ok
}
func (s *SafeMap) Write(key string, value int) {
s.mu.Lock()
defer s.mu.Unlock()
s.m[key] = value
}
func main() {
s := SafeMap{m: make(map[string]int)}
var wg sync.WaitGroup
// 寫入數據
wg.Add(1)
go func() {
defer wg.Done()
for i := 0; i < 5; i++ {
s.Write(fmt.Sprintf("key%d", i), i)
time.Sleep(100 * time.Millisecond)
}
}()
// 讀取數據
for i := 0; i < 5; i++ {
wg.Add(1)
go func(id int) {
defer wg.Done()
val, ok := s.Read(fmt.Sprintf("key%d", id))
if ok {
fmt.Printf("讀取到 key%d: %d\n", id, val)
} else {
fmt.Printf("key%d 不存在\n", id)
}
}(i)
}
wg.Wait()
fmt.Println("所有操作完成")
}
說明:
SafeMap
結構,使用 RWMutex
保護內部的 map。Read
方法使用 RLock
進行讀操作,允許多個讀操作同時進行。Write
方法使用 Lock
進行寫操作,確保寫操作的獨佔性。那麼今天的文章就到這告一段落,如果我的文章有任何地方有錯誤請在留言區反應
明天將會介紹Go語言的Package的基本概念及創建方法