はじめに
Go言語では、並行処理を制御するためのツールとして、コンテキスト(context)と同期プリミティブが提供されています。本記事では、これらの活用方法を基礎から解説し、実践的な例を紹介します。
コンテキスト(context)とは?
コンテキスト(context)は、Goの標準ライブラリで提供される機能で、並行処理における「キャンセル」「タイムアウト」「値の伝播」を管理するために使用されます。
典型的なシナリオ:
- HTTPリクエストがキャンセルされたときに、関連するgoroutineを停止させる。
- 一定時間内に処理を完了させるタイムアウト制御。
コンテキストの基本操作
package main
import (
"context"
"fmt"
"time"
)
func main() {
// タイムアウト付きのコンテキストを作成
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
defer cancel() // 必ずキャンセル関数を呼ぶ
// goroutineで処理を実行
go func(ctx context.Context) {
for {
select {
case <-ctx.Done(): // タイムアウト時の処理
fmt.Println("Context canceled:", ctx.Err())
return
default:
fmt.Println("Working...")
time.Sleep(500 * time.Millisecond)
}
}
}(ctx)
// メイン関数で待機
time.Sleep(3 * time.Second)
fmt.Println("Main function finished")
}
このコードでは、2秒後にタイムアウトが発生し、goroutineが終了します。context.WithTimeout
を利用することで、簡単にタイムアウト制御が可能です。
コンテキストのキャンセル操作
手動でキャンセルを実行する場合には、context.WithCancel
を使用します。
package main
import (
"context"
"fmt"
"time"
)
func main() {
// キャンセル可能なコンテキストを作成
ctx, cancel := context.WithCancel(context.Background())
// goroutineを起動
go func(ctx context.Context) {
for {
select {
case <-ctx.Done():
fmt.Println("Canceled")
return
default:
fmt.Println("Running...")
time.Sleep(500 * time.Millisecond)
}
}
}(ctx)
// 2秒後にキャンセル
time.Sleep(2 * time.Second)
cancel()
time.Sleep(1 * time.Second)
}
同期プリミティブとは?
Goの同期プリミティブは、複数のgoroutine間でデータを安全に共有するために使用されます。主な同期プリミティブには以下があります:
-
sync.Mutex
(ミューテックス) -
sync.WaitGroup
(ウェイトグループ) -
sync.Once
(一度だけ実行) -
sync.Cond
(条件変数)
Mutex(ミューテックス)の使い方
sync.Mutex
は、共有データへの同時アクセスを防ぐために使用します。
package main
import (
"fmt"
"sync"
"time"
)
func main() {
var mu sync.Mutex
counter := 0
increment := func() {
mu.Lock() // ロック
defer mu.Unlock() // 処理終了後にアンロック
counter++
fmt.Println("Counter:", counter)
}
// 複数のgoroutineで実行
for i := 0; i < 5; i++ {
go increment()
}
time.Sleep(1 * time.Second)
}
この例では、mu.Lock
とmu.Unlock
で保護することで、counter
に安全にアクセスできます。
WaitGroup(ウェイトグループ)の使い方
sync.WaitGroup
は、複数のgoroutineが終了するのを待機するために使用されます。
package main
import (
"fmt"
"sync"
"time"
)
func main() {
var wg sync.WaitGroup
worker := func(id int) {
defer wg.Done() // 作業終了を通知
fmt.Printf("Worker %d starting\n", id)
time.Sleep(time.Second) // 処理に1秒
fmt.Printf("Worker %d done\n", id)
}
// 3つのgoroutineを起動
for i := 1; i <= 3; i++ {
wg.Add(1) // goroutineを追加
go worker(i)
}
wg.Wait() // すべてのgoroutineが終了するのを待つ
fmt.Println("All workers finished")
}
Once(一度だけ実行)の使い方
sync.Once
を使うと、処理を一度だけ実行することを保証できます。
package main
import (
"fmt"
"sync"
)
func main() {
var once sync.Once
init := func() {
fmt.Println("Initialization")
}
for i := 0; i < 3; i++ {
go func() {
once.Do(init) // 一度だけ実行
}()
}
fmt.Scanln() // 終了を防ぐ
}
まとめ
Go言語のコンテキスト(context)と同期プリミティブを活用することで、複雑な並行処理をシンプルに実現できます。
- コンテキストはタイムアウトやキャンセル処理に便利。
- 同期プリミティブはデータの安全な共有やgoroutineの調整に役立つ。