Goのプログラミングにおいて、goroutine
は並行処理の重要な要素です。しかし、多数のgoroutine
が実行される場面では、適切な制御や終了が求められることがあります。この課題を解決するために登場するのがcontext
パッケージです。本記事では、context
を活用してgoroutine
を効率的に制御し、キャンセル処理を実装する方法を分かりやすく解説します。これにより、並行処理をより安全かつ効果的に管理するための知識が得られるでしょう。
Contextパッケージの基本概要
Goのcontext
パッケージは、goroutine
間で終了シグナルやデータを共有するための仕組みを提供します。これにより、並行処理を柔軟かつ安全に制御することが可能になります。
Contextの主な役割
- キャンセルシグナルの伝達: 複数の
goroutine
に対して、一括でキャンセルを指示できます。 - タイムアウトの設定: 一定時間が経過したら自動的に処理を終了させることができます。
- 値の伝播: 必要なデータを
goroutine
間で効率的に共有できます。
主要なContextの生成方法
context.Background()
: 最上位のコンテキストを生成します。アプリケーションのルートとして使用されます。context.TODO()
: コンテキストをまだ決定していない場合に使用するプレースホルダーです。
Contextの主要な型
Context
インターフェース: メソッドを通じてキャンセル信号や値を管理します。Done()
: キャンセル信号を受け取るためのチャネルを返します。Err()
: コンテキストが終了した理由を返します。Value(key interface{})
: コンテキストに関連付けられた値を取得します。
context
パッケージは、効率的なリソース管理と安全な並行処理を実現するための重要なツールです。次節では、goroutine
との具体的な連携方法を詳しく見ていきます。
GoroutineとContextの関係性
Goroutineの並行処理における課題
Goのgoroutine
は軽量で効率的な並行処理を実現しますが、以下のような課題があります:
- 制御の難しさ: 一度起動した
goroutine
を適切に終了させる手段が乏しい。 - リソースの浪費: 必要のない
goroutine
が実行を続けると、メモリやCPUリソースを浪費する。 - 同期の困難さ: 複数の
goroutine
が絡む複雑な処理では、同期が難しくなる。
これらの課題に対処するために、context
が役立ちます。
Contextが解決する課題
- 終了の統一管理:
context
を利用すると、複数のgoroutine
を一括でキャンセルできます。 - タイムアウト管理:
context
を用いて、処理に時間制限を設けることが可能です。 - データの安全な伝播: 親子関係にある
goroutine
間で必要なデータを安全に共有できます。
ContextとGoroutineの連携フロー
- Contextの生成:
親goroutine
でcontext.WithCancel
やcontext.WithTimeout
を使ってcontext
を生成します。 - Contextの渡し方:
子goroutine
に生成したcontext
を渡します。 - キャンセルシグナルの監視:
子goroutine
内でDone()
チャネルを監視し、キャンセルシグナルを受け取ったら適切に終了します。
コード例: GoroutineとContextの連携
package main
import (
"context"
"fmt"
"time"
)
func main() {
ctx, cancel := context.WithCancel(context.Background())
go func(ctx context.Context) {
for {
select {
case <-ctx.Done():
fmt.Println("Goroutine canceled")
return
default:
fmt.Println("Working...")
time.Sleep(500 * time.Millisecond)
}
}
}(ctx)
time.Sleep(2 * time.Second)
cancel()
time.Sleep(1 * time.Second)
}
結果の説明
cancel()
が呼び出されると、子goroutine
はctx.Done()
チャネルを受信して終了します。- これにより、リソースの効率的な解放と安全な
goroutine
管理が実現できます。
このように、context
はgoroutine
の制御において欠かせないツールです。次節では、context
の生成方法とその親子関係について詳しく解説します。
Contextの生成と親子関係
Contextの生成方法
Goのcontext
は、親子関係を持つことでgoroutine
間で統一的に制御できる仕組みを提供します。まずは基本的なcontext
の生成方法を見ていきましょう。
1. `context.Background()`
最初に作成される親となるcontext
で、アプリケーション全体のルートとして使用されます。
ctx := context.Background()
2. `context.WithCancel()`
キャンセル可能な子context
を生成します。親のcontext
がキャンセルされると、この子も自動的にキャンセルされます。
ctx, cancel := context.WithCancel(context.Background())
3. `context.WithTimeout()`
指定した時間が経過すると自動でキャンセルされるcontext
を生成します。
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
4. `context.WithValue()`
親context
にキーと値を紐づけた新しいcontext
を生成します。
ctx := context.WithValue(context.Background(), "key", "value")
Contextの親子関係
親子関係の仕組み
context
には親子関係が存在し、親context
がキャンセルされると、全ての子context
がキャンセルされます。この機能により、一括制御が可能になります。
親子関係の例
以下のコードは、親context
とその子context
の関係を示しています。
package main
import (
"context"
"fmt"
"time"
)
func main() {
parentCtx := context.Background()
ctx, cancel := context.WithCancel(parentCtx)
go func(ctx context.Context) {
for {
select {
case <-ctx.Done():
fmt.Println("Child context canceled")
return
default:
fmt.Println("Child goroutine working")
time.Sleep(500 * time.Millisecond)
}
}
}(ctx)
time.Sleep(2 * time.Second)
cancel()
time.Sleep(1 * time.Second)
}
結果の動作
- 親
context
でcancel()
が呼び出されると、子goroutine
が即座にキャンセルされます。 - これにより、親子間で効率的な制御が可能となります。
親子関係のメリット
- 一括キャンセル: 親
context
をキャンセルするだけで、全ての子が自動的に終了します。 - コードの簡素化: 親子関係を利用することで、複雑な
goroutine
管理がシンプルになります。 - 効率的なリソース解放: 不要な
goroutine
が速やかに終了するため、リソースを節約できます。
次節では、この親子関係を応用したキャンセル処理の実装例についてさらに詳しく解説します。
キャンセル処理の実装例
context
を使うと、goroutine
の終了処理を安全かつ簡潔に実装できます。ここでは、context.WithCancel
を利用した具体的なキャンセル処理の方法を解説します。
キャンセル処理の基本的な仕組み
context.WithCancel
でキャンセル可能なcontext
を生成
親context
に基づいてキャンセル可能な子context
を生成します。goroutine
内でcontext.Done()
を監視Done()
チャネルを監視し、キャンセルシグナルが送信された際に処理を終了します。- 親から
cancel()
を呼び出す
必要に応じて、親側からcancel()
を呼び出し、全ての子goroutine
を終了させます。
キャンセル処理の実装例
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("Goroutine canceled")
return
default:
fmt.Println("Goroutine working")
time.Sleep(500 * time.Millisecond)
}
}
}(ctx)
// メイン処理
time.Sleep(2 * time.Second)
fmt.Println("Calling cancel function...")
cancel() // キャンセルシグナルを送信
time.Sleep(1 * time.Second) // 子goroutineの終了を待機
}
コードの動作
- メイン処理で
context.WithCancel
を使用してキャンセル可能なcontext
を生成します。 - 子
goroutine
内ではcontext.Done()
を監視し、キャンセルシグナルを受け取った際にreturn
で終了します。 - 2秒後に
cancel()
が呼び出され、キャンセルシグナルが送信されます。子goroutine
は安全に終了します。
出力結果
Goroutine working
Goroutine working
Goroutine working
Calling cancel function...
Goroutine canceled
応用ポイント
- 複数の
goroutine
でのキャンセル: 親context
を共有する複数のgoroutine
を一括制御できます。 - 即時終了:
Done()
チャネルの信号を受け取った時点で速やかに処理が停止します。 - リソース管理: 必要のない
goroutine
が終了することで、システムリソースを無駄なく利用できます。
次節では、context.WithTimeout
を用いたタイムアウト処理について解説し、キャンセルの応用例をさらに深掘りします。
Contextによるタイムアウト設定
Goのcontext
パッケージを使うと、指定した時間が経過した際に自動的にキャンセルシグナルを送ることができます。これにより、時間制限が必要な処理を安全かつ効率的に管理できます。
タイムアウト設定の基本
context.WithTimeout
を使用すると、新しいcontext
を生成する際にタイムアウトを設定できます。このcontext
は、指定した時間が経過すると自動的にキャンセルされます。
構文
ctx, cancel := context.WithTimeout(parentContext, timeout)
parentContext
: 親context
を指定します。timeout
: タイムアウトまでの時間を指定します。
タイムアウトの実装例
package main
import (
"context"
"fmt"
"time"
)
func main() {
// 3秒のタイムアウトを設定
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
defer cancel() // リソース解放のために必ず呼び出す
// 子goroutineを起動
go func(ctx context.Context) {
for {
select {
case <-ctx.Done():
fmt.Println("Goroutine canceled due to timeout")
return
default:
fmt.Println("Goroutine working...")
time.Sleep(500 * time.Millisecond)
}
}
}(ctx)
// メイン処理でタイムアウトを待つ
time.Sleep(5 * time.Second)
fmt.Println("Main function finished")
}
コードの動作
context.WithTimeout
を使用して、タイムアウトが3秒に設定されたcontext
を生成します。- 子
goroutine
では、Done()
チャネルを監視し、タイムアウトが発生したら終了します。 - メイン処理は5秒間待機しますが、
goroutine
は3秒経過時点でキャンセルされます。
出力結果
Goroutine working...
Goroutine working...
Goroutine working...
Goroutine working...
Goroutine canceled due to timeout
Main function finished
タイムアウト設定の応用
- APIリクエストのタイムアウト: 外部API呼び出しの失敗時にリソースを無駄にしないためのタイムアウト設定。
- バックグラウンド処理の制限: 長時間かかる処理を制限することで、アプリケーション全体の応答性を確保。
- データベースクエリの制御: クエリ処理に時間がかかりすぎる場合の強制終了。
注意点
cancel()
の呼び出し: タイムアウトが発生しなくても、cancel()
を呼び出してリソースを解放する必要があります。- 正確なタイムアウト設定: タイムアウト時間を適切に設定し、過剰なキャンセルを防ぐように設計します。
次節では、context.WithValue
を使った値の伝播機能について解説し、さらなる活用例を紹介します。
Contextの値伝播機能
Goのcontext
パッケージには、context.WithValue
を使用してキーと値を関連付ける機能があります。この機能により、親から子のgoroutine
へデータを安全に伝播させることが可能です。以下では、この機能の使い方と応用例を解説します。
Contextでの値の伝播
context.WithValue
を使用すると、新しいcontext
にキーと値を関連付けた情報を追加できます。生成されたcontext
は親のcontext
を引き継ぐため、親から子goroutine
へ値を伝播できます。
構文
ctx := context.WithValue(parentContext, key, value)
parentContext
: 親context
を指定します。key
: 値を関連付けるためのキー(任意の型)。value
: 関連付ける値(任意の型)。
値の取得
関連付けた値を取得するには、Context.Value(key)
を使用します。
- 存在しないキーの場合は
nil
が返されます。
値伝播の実装例
package main
import (
"context"
"fmt"
)
func main() {
// 親コンテキストに値を設定
ctx := context.WithValue(context.Background(), "userID", 42)
// 子goroutineで値を取得
go func(ctx context.Context) {
if userID := ctx.Value("userID"); userID != nil {
fmt.Printf("User ID: %v\n", userID)
} else {
fmt.Println("User ID not found")
}
}(ctx)
// 処理の待機
fmt.Scanln()
}
出力結果
User ID: 42
応用例
1. リクエストごとのトレーシング情報の管理
goroutine
間でリクエストIDを伝播することで、分散トレースやログの管理が容易になります。
ctx := context.WithValue(context.Background(), "requestID", "12345")
2. 設定情報の伝播
サービス設定や認証情報などを各goroutine
に安全に渡します。
3. ユーザー認証
ユーザーセッションや認証トークンを親context
から子goroutine
に伝播させることで、効率的なユーザー管理を実現します。
注意点
- 使用するデータは軽量に:
context
はキャンセルやタイムアウト制御のための設計であり、大量のデータ伝播には適していません。 - キーには適切な型を使用: 推奨される方法として、キーにはカスタム型を使用して衝突を防ぎます。
安全なキーの定義例
type contextKey string
const userIDKey contextKey = "userID"
ctx := context.WithValue(context.Background(), userIDKey, 42)
次節では、Goroutine制御におけるcontext
のベストプラクティスを紹介し、効率的な設計方法を解説します。
Goroutineのキャンセル処理でのベストプラクティス
context
を活用したgoroutine
のキャンセル処理では、適切な設計と実装が重要です。不適切な使用はリソースリークや不安定な挙動を引き起こす可能性があります。以下に、効果的な設計方法とベストプラクティスを紹介します。
1. キャンセル処理を設計に組み込む
キャンセル可能な処理が必要な場合は、常にcontext
を第一引数として関数に渡す設計を採用しましょう。
func performTask(ctx context.Context) {
select {
case <-ctx.Done():
fmt.Println("Task canceled")
return
default:
// タスクの実行
fmt.Println("Task running")
}
}
理由
- 関数がどのように終了すべきかを明示的に設計できます。
- 複数の呼び出し元で再利用可能なコードになります。
2. 必要に応じて早期キャンセルを行う
長時間実行されるタスクでは、キャンセルのシグナルを即座にチェックして早期終了を実現します。
実装例
func longRunningTask(ctx context.Context) {
for i := 0; i < 10; i++ {
select {
case <-ctx.Done():
fmt.Println("Canceled at iteration", i)
return
default:
fmt.Println("Working on iteration", i)
time.Sleep(1 * time.Second)
}
}
}
ポイント
- 反復処理の中でキャンセルシグナルを監視すること。
- 必要のない処理を回避し、リソースの浪費を防ぎます。
3. コンテキストを正しく解放する
context.WithCancel
やcontext.WithTimeout
を使用した場合、必ずcancel()
を呼び出してリソースを解放してください。
悪い例
ctx, _ := context.WithCancel(context.Background())
// cancel()を呼び出していない
良い例
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
4. 複数のGoroutineを効率的に管理する
複数のgoroutine
を制御する際は、1つのcontext
を共有することで、全てのgoroutine
を一括制御できます。
実装例
func worker(ctx context.Context, id int) {
for {
select {
case <-ctx.Done():
fmt.Printf("Worker %d canceled\n", id)
return
default:
fmt.Printf("Worker %d working\n", id)
time.Sleep(1 * time.Second)
}
}
}
func main() {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
for i := 1; i <= 3; i++ {
go worker(ctx, i)
}
time.Sleep(3 * time.Second)
cancel()
time.Sleep(1 * time.Second)
}
結果
- 全ての
goroutine
がキャンセルされ、安全に終了します。
5. エラー処理を忘れない
context.Err()
を使用して、終了理由を確認し、適切なエラーハンドリングを行います。
func taskWithErrorHandling(ctx context.Context) {
select {
case <-ctx.Done():
if ctx.Err() == context.Canceled {
fmt.Println("Task was canceled by user")
} else if ctx.Err() == context.DeadlineExceeded {
fmt.Println("Task timed out")
}
}
}
まとめ
context
を使ったキャンセル処理を設計の中心に据える。- 必要に応じてキャンセルシグナルを常時監視する。
cancel()
を忘れずに呼び出し、リソースリークを防ぐ。- 複数の
goroutine
を効率的に制御するために、context
を活用する。
次節では、これらの設計指針を応用した複数のgoroutine
を一括管理する方法を具体的に解説します。
応用例:複数のGoroutineの制御
複数のgoroutine
を効率的に管理するためには、context
を活用して一括制御する方法が有効です。このセクションでは、複数のgoroutine
をcontext
で管理し、キャンセルやタイムアウトを適用する具体的な方法を解説します。
複数のGoroutineを一括キャンセルする例
実装コード
以下の例では、複数のgoroutine
を起動し、親context
から一括でキャンセルシグナルを送信します。
package main
import (
"context"
"fmt"
"time"
)
func worker(ctx context.Context, id int) {
for {
select {
case <-ctx.Done():
fmt.Printf("Worker %d stopped\n", id)
return
default:
fmt.Printf("Worker %d is working...\n", id)
time.Sleep(1 * time.Second)
}
}
}
func main() {
ctx, cancel := context.WithCancel(context.Background())
// 3つのworkerを起動
for i := 1; i <= 3; i++ {
go worker(ctx, i)
}
// 3秒後にキャンセル
time.Sleep(3 * time.Second)
fmt.Println("Cancelling all workers...")
cancel()
// 少し待機して終了
time.Sleep(1 * time.Second)
fmt.Println("Main function finished")
}
出力結果
Worker 1 is working...
Worker 2 is working...
Worker 3 is working...
Worker 1 is working...
Worker 2 is working...
Worker 3 is working...
Cancelling all workers...
Worker 1 stopped
Worker 2 stopped
Worker 3 stopped
Main function finished
解説
- 各
worker
関数はctx.Done()
を監視し、キャンセルシグナルを受け取ると安全に終了します。 - 親の
cancel()
を呼び出すことで、全ての子goroutine
が一括で停止します。
タイムアウトを適用したGoroutineの管理
実装コード
以下の例では、タイムアウトを設定し、指定時間内に処理が完了しない場合にgoroutine
を自動的に停止します。
package main
import (
"context"
"fmt"
"time"
)
func workerWithTimeout(ctx context.Context, id int) {
for {
select {
case <-ctx.Done():
fmt.Printf("Worker %d timed out\n", id)
return
default:
fmt.Printf("Worker %d is working...\n", id)
time.Sleep(1 * time.Second)
}
}
}
func main() {
// 5秒のタイムアウトを設定
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
// 2つのworkerを起動
for i := 1; i <= 2; i++ {
go workerWithTimeout(ctx, i)
}
// 処理終了を待つ
time.Sleep(6 * time.Second)
fmt.Println("Main function finished")
}
出力結果
Worker 1 is working...
Worker 2 is working...
Worker 1 is working...
Worker 2 is working...
Worker 1 is working...
Worker 2 is working...
Worker 1 timed out
Worker 2 timed out
Main function finished
解説
- 親
context
にタイムアウトを設定し、指定時間内に処理が完了しない場合は自動的にキャンセルされます。 Done()
チャネルを監視することで、タイムアウト後に安全にgoroutine
が終了します。
複数のGoroutineをWaitGroupとContextで組み合わせる
sync.WaitGroup
とcontext
を組み合わせることで、複数のgoroutine
の終了を待つ実装が可能です。
実装コード
package main
import (
"context"
"fmt"
"sync"
"time"
)
func workerWithWaitGroup(ctx context.Context, wg *sync.WaitGroup, id int) {
defer wg.Done()
for {
select {
case <-ctx.Done():
fmt.Printf("Worker %d stopped\n", id)
return
default:
fmt.Printf("Worker %d is working...\n", id)
time.Sleep(1 * time.Second)
}
}
}
func main() {
var wg sync.WaitGroup
ctx, cancel := context.WithCancel(context.Background())
// 3つのworkerを起動
for i := 1; i <= 3; i++ {
wg.Add(1)
go workerWithWaitGroup(ctx, &wg, i)
}
// 5秒後にキャンセル
time.Sleep(5 * time.Second)
fmt.Println("Cancelling all workers...")
cancel()
// 全てのworkerが終了するのを待機
wg.Wait()
fmt.Println("All workers finished")
}
出力結果
Worker 1 is working...
Worker 2 is working...
Worker 3 is working...
Cancelling all workers...
Worker 1 stopped
Worker 2 stopped
Worker 3 stopped
All workers finished
解説
sync.WaitGroup
でgoroutine
の完了を待機することで、メイン処理が早期終了するのを防ぎます。context
とWaitGroup
の組み合わせにより、安全かつ効率的にgoroutine
を管理できます。
まとめ
context
は複数のgoroutine
を一括制御する強力なツールです。- タイムアウトやキャンセルを活用することで、効率的な並行処理が実現できます。
- 必要に応じて
sync.WaitGroup
を組み合わせ、全てのgoroutine
が安全に終了するまで待機する設計を採用しましょう。
次節では、context
使用時によく発生するエラーとそのトラブルシューティングについて解説します。
よくあるエラーとトラブルシューティング
context
を使用する際には、特定の状況下でエラーが発生することがあります。これらの問題を事前に把握し、適切に対処することで、スムーズな開発が可能になります。このセクションでは、context
関連のよくあるエラーとその解決策を解説します。
1. `context deadline exceeded` エラー
問題の概要
このエラーは、設定されたタイムアウトやデッドラインを超えた場合に発生します。例えば、外部APIリクエストが遅延し、タイムアウトに達するとこのエラーが返されます。
解決方法
- タイムアウトの延長: 必要に応じてタイムアウト時間を適切に設定します。
- 処理の分割: 長時間かかる処理を小さなタスクに分割し、それぞれに適切なタイムアウトを設定します。
修正例
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
2. `context canceled` エラー
問題の概要
このエラーは、親context
のcancel()
が呼び出された場合に発生します。たとえば、親がキャンセルシグナルを送信した後も子goroutine
が処理を続行しようとすると、このエラーが返されます。
解決方法
- キャンセル処理を適切に実装: 子
goroutine
内でctx.Done()
を監視し、キャンセル時に処理を速やかに終了します。 - リソースの解放: キャンセル後に使用されるリソースを確実に解放します。
修正例
select {
case <-ctx.Done():
fmt.Println("Task canceled")
return
default:
// 処理の実行
}
3. キーの衝突による値取得の失敗
問題の概要
context.WithValue
を使用する際、文字列や基本型のキーを使うと、キーが衝突して値の取得に失敗する可能性があります。
解決方法
- カスタム型のキーを使用: コンフリクトを防ぐため、ユニークなカスタム型をキーとして使用します。
修正例
type contextKey string
const userIDKey contextKey = "userID"
ctx := context.WithValue(context.Background(), userIDKey, 42)
userID := ctx.Value(userIDKey)
4. `cancel()`の呼び出し忘れ
問題の概要
context.WithCancel
やcontext.WithTimeout
で生成したcontext
のcancel()
を呼び出さないと、リソースが解放されずリークの原因となります。
解決方法
- 必ず
defer
でcancel()
を呼び出す:context
生成直後にdefer
を使用してキャンセル処理を記述します。
修正例
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
5. タイムアウトとキャンセルの競合
問題の概要
context.WithTimeout
とcontext.WithCancel
を同時に使用する場合、キャンセル処理とタイムアウト処理が競合して不安定な挙動を引き起こすことがあります。
解決方法
- シンプルな設計を心がける: 同じ
context
にタイムアウトとキャンセルを混在させるのではなく、役割ごとにcontext
を分割します。
修正例
timeoutCtx, timeoutCancel := context.WithTimeout(context.Background(), 5*time.Second)
defer timeoutCancel()
cancelCtx, cancel := context.WithCancel(timeoutCtx)
defer cancel()
まとめ
context deadline exceeded
やcontext canceled
エラーは、タイムアウトやキャンセル処理を適切に設計することで防げます。context.WithValue
を使用する際は、キーの衝突を防ぐためにカスタム型を使用します。cancel()
の呼び出し忘れを防ぐためにdefer
を活用しましょう。- タイムアウトとキャンセルの競合を避けるために、用途ごとに
context
を分割する設計を心がけます。
次節では、学んだ内容を深めるための演習問題を提供し、解答例を解説します。
演習問題と解答例
ここまで学んだ内容を実践するために、以下の演習問題を解いてみましょう。context
を用いたGoroutine制御に関する基本的な問題から応用問題まで含まれています。解答例も提供していますので、理解を深める参考にしてください。
演習問題
問題1: Goroutineのキャンセル処理
context.WithCancel
を使用して、以下の要件を満たすプログラムを作成してください。
- 子
goroutine
が"Processing..."
を出力し続ける。 - メイン処理で
cancel()
を呼び出したら、子goroutine
が終了し"Goroutine stopped"
を出力する。
問題2: タイムアウト処理
context.WithTimeout
を使用して、以下の要件を満たすプログラムを作成してください。
- 子
goroutine
が"Working..."
を1秒ごとに出力する。 - 5秒のタイムアウトが経過すると、子
goroutine
が終了し"Task timed out"
を出力する。
問題3: 複数のGoroutineの管理
sync.WaitGroup
とcontext
を組み合わせて以下の要件を満たすプログラムを作成してください。
- 3つの子
goroutine
がそれぞれ異なるIDを出力しながら処理を行う。 - 親
context
でキャンセルシグナルを送信した場合、全てのgoroutine
が終了する。 - 全ての子
goroutine
が終了するのを待機して、最後に"All workers finished"
を出力する。
解答例
解答1: Goroutineのキャンセル処理
package main
import (
"context"
"fmt"
"time"
)
func main() {
ctx, cancel := context.WithCancel(context.Background())
go func(ctx context.Context) {
for {
select {
case <-ctx.Done():
fmt.Println("Goroutine stopped")
return
default:
fmt.Println("Processing...")
time.Sleep(1 * time.Second)
}
}
}(ctx)
time.Sleep(3 * time.Second)
cancel()
time.Sleep(1 * time.Second)
}
解答2: タイムアウト処理
package main
import (
"context"
"fmt"
"time"
)
func main() {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
go func(ctx context.Context) {
for {
select {
case <-ctx.Done():
fmt.Println("Task timed out")
return
default:
fmt.Println("Working...")
time.Sleep(1 * time.Second)
}
}
}(ctx)
time.Sleep(6 * time.Second)
}
解答3: 複数のGoroutineの管理
package main
import (
"context"
"fmt"
"sync"
"time"
)
func worker(ctx context.Context, wg *sync.WaitGroup, id int) {
defer wg.Done()
for {
select {
case <-ctx.Done():
fmt.Printf("Worker %d stopped\n", id)
return
default:
fmt.Printf("Worker %d is working...\n", id)
time.Sleep(1 * time.Second)
}
}
}
func main() {
var wg sync.WaitGroup
ctx, cancel := context.WithCancel(context.Background())
for i := 1; i <= 3; i++ {
wg.Add(1)
go worker(ctx, &wg, i)
}
time.Sleep(3 * time.Second)
fmt.Println("Cancelling all workers...")
cancel()
wg.Wait()
fmt.Println("All workers finished")
}
解説
- 問題1では、
context.WithCancel
を用いて子goroutine
の安全な終了を実現しました。 - 問題2では、
context.WithTimeout
でタイムアウト制御を行い、時間制限内に処理が終了しない場合の自動停止を実現しました。 - 問題3では、
sync.WaitGroup
とcontext
を組み合わせ、複数のgoroutine
の終了を管理し、メイン処理の待機を実現しました。
次節では、本記事の内容を簡潔にまとめ、学びを整理します。
まとめ
本記事では、Goのcontext
パッケージを活用したgoroutine
の制御とキャンセル処理について学びました。context
は並行処理を効率的に管理し、安全に制御するための重要なツールであり、特に以下の点が重要です:
重要ポイント
context
の基本的な使い方
context.Background()
やcontext.TODO()
で親context
を生成し、context.WithCancel
やcontext.WithTimeout
でキャンセルやタイムアウトを管理できます。
goroutine
とcontext
の連携
goroutine
内でctx.Done()
を監視し、キャンセルシグナルを受け取った際に処理を安全に終了させることができます。
- 複数の
goroutine
を一括制御
- 親
context
を共有することで、複数のgoroutine
を一括でキャンセルできます。また、sync.WaitGroup
を組み合わせて、全てのgoroutine
の終了を待つことが可能です。
- タイムアウトとキャンセル処理
context.WithTimeout
を利用して、時間制限内に処理を終わらせることができます。context.WithCancel
を用いて、親context
がキャンセルされた際に全ての子goroutine
を一括で終了させることができます。
- エラー処理とトラブルシューティング
context deadline exceeded
やcontext canceled
といったエラーが発生する場合、適切なタイムアウト設定やキャンセル処理を行うことで防げます。
学習のまとめ
Goのcontext
は、並行処理を扱う際に非常に強力なツールです。context
を適切に活用することで、プログラムの安定性や効率性を大幅に向上させることができます。この記事を通して、context
の使い方、エラーハンドリング、複数のgoroutine
の制御方法を学びました。
今後、並行処理やキャンセル制御が求められるシステムにおいて、context
を積極的に活用し、スケーラブルで効率的なコードを作成できるようになるでしょう。
コメント