GoのContextで学ぶGoroutineの制御とキャンセル処理入門

Goのプログラミングにおいて、goroutineは並行処理の重要な要素です。しかし、多数のgoroutineが実行される場面では、適切な制御や終了が求められることがあります。この課題を解決するために登場するのがcontextパッケージです。本記事では、contextを活用してgoroutineを効率的に制御し、キャンセル処理を実装する方法を分かりやすく解説します。これにより、並行処理をより安全かつ効果的に管理するための知識が得られるでしょう。

目次
  1. Contextパッケージの基本概要
    1. Contextの主な役割
    2. 主要なContextの生成方法
    3. Contextの主要な型
  2. GoroutineとContextの関係性
    1. Goroutineの並行処理における課題
    2. Contextが解決する課題
    3. ContextとGoroutineの連携フロー
    4. 結果の説明
  3. Contextの生成と親子関係
    1. Contextの生成方法
    2. Contextの親子関係
    3. 親子関係のメリット
  4. キャンセル処理の実装例
    1. キャンセル処理の基本的な仕組み
    2. キャンセル処理の実装例
    3. コードの動作
    4. 出力結果
    5. 応用ポイント
  5. Contextによるタイムアウト設定
    1. タイムアウト設定の基本
    2. タイムアウトの実装例
    3. コードの動作
    4. 出力結果
    5. タイムアウト設定の応用
    6. 注意点
  6. Contextの値伝播機能
    1. Contextでの値の伝播
    2. 値伝播の実装例
    3. 出力結果
    4. 応用例
    5. 注意点
  7. Goroutineのキャンセル処理でのベストプラクティス
    1. 1. キャンセル処理を設計に組み込む
    2. 2. 必要に応じて早期キャンセルを行う
    3. 3. コンテキストを正しく解放する
    4. 4. 複数のGoroutineを効率的に管理する
    5. 5. エラー処理を忘れない
    6. まとめ
  8. 応用例:複数のGoroutineの制御
    1. 複数のGoroutineを一括キャンセルする例
    2. タイムアウトを適用したGoroutineの管理
    3. 複数のGoroutineをWaitGroupとContextで組み合わせる
    4. まとめ
  9. よくあるエラーとトラブルシューティング
    1. 1. `context deadline exceeded` エラー
    2. 2. `context canceled` エラー
    3. 3. キーの衝突による値取得の失敗
    4. 4. `cancel()`の呼び出し忘れ
    5. 5. タイムアウトとキャンセルの競合
    6. まとめ
  10. 演習問題と解答例
    1. 演習問題
    2. 解答例
    3. 解説
  11. まとめ
    1. 重要ポイント
    2. 学習のまとめ

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の連携フロー

  1. Contextの生成:
    goroutinecontext.WithCancelcontext.WithTimeoutを使ってcontextを生成します。
  2. Contextの渡し方:
    goroutineに生成したcontextを渡します。
  3. キャンセルシグナルの監視:
    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()が呼び出されると、子goroutinectx.Done()チャネルを受信して終了します。
  • これにより、リソースの効率的な解放と安全なgoroutine管理が実現できます。

このように、contextgoroutineの制御において欠かせないツールです。次節では、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)
}

結果の動作

  • contextcancel()が呼び出されると、子goroutineが即座にキャンセルされます。
  • これにより、親子間で効率的な制御が可能となります。

親子関係のメリット

  1. 一括キャンセル: 親contextをキャンセルするだけで、全ての子が自動的に終了します。
  2. コードの簡素化: 親子関係を利用することで、複雑なgoroutine管理がシンプルになります。
  3. 効率的なリソース解放: 不要なgoroutineが速やかに終了するため、リソースを節約できます。

次節では、この親子関係を応用したキャンセル処理の実装例についてさらに詳しく解説します。

キャンセル処理の実装例

contextを使うと、goroutineの終了処理を安全かつ簡潔に実装できます。ここでは、context.WithCancelを利用した具体的なキャンセル処理の方法を解説します。

キャンセル処理の基本的な仕組み

  1. context.WithCancelでキャンセル可能なcontextを生成
    contextに基づいてキャンセル可能な子contextを生成します。
  2. goroutine内でcontext.Done()を監視
    Done()チャネルを監視し、キャンセルシグナルが送信された際に処理を終了します。
  3. 親から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の終了を待機
}

コードの動作

  1. メイン処理でcontext.WithCancelを使用してキャンセル可能なcontextを生成します。
  2. goroutine内ではcontext.Done()を監視し、キャンセルシグナルを受け取った際にreturnで終了します。
  3. 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")
}

コードの動作

  1. context.WithTimeoutを使用して、タイムアウトが3秒に設定されたcontextを生成します。
  2. goroutineでは、Done()チャネルを監視し、タイムアウトが発生したら終了します。
  3. メイン処理は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.WithCancelcontext.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を活用して一括制御する方法が有効です。このセクションでは、複数のgoroutinecontextで管理し、キャンセルやタイムアウトを適用する具体的な方法を解説します。

複数の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.WaitGroupcontextを組み合わせることで、複数の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.WaitGroupgoroutineの完了を待機することで、メイン処理が早期終了するのを防ぎます。
  • contextWaitGroupの組み合わせにより、安全かつ効率的に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` エラー

問題の概要


このエラーは、親contextcancel()が呼び出された場合に発生します。たとえば、親がキャンセルシグナルを送信した後も子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.WithCancelcontext.WithTimeoutで生成したcontextcancel()を呼び出さないと、リソースが解放されずリークの原因となります。

解決方法

  • 必ずdefercancel()を呼び出す: context生成直後にdeferを使用してキャンセル処理を記述します。

修正例

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

5. タイムアウトとキャンセルの競合

問題の概要


context.WithTimeoutcontext.WithCancelを同時に使用する場合、キャンセル処理とタイムアウト処理が競合して不安定な挙動を引き起こすことがあります。

解決方法

  • シンプルな設計を心がける: 同じcontextにタイムアウトとキャンセルを混在させるのではなく、役割ごとにcontextを分割します。

修正例

timeoutCtx, timeoutCancel := context.WithTimeout(context.Background(), 5*time.Second)
defer timeoutCancel()

cancelCtx, cancel := context.WithCancel(timeoutCtx)
defer cancel()

まとめ

  • context deadline exceededcontext 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.WaitGroupcontextを組み合わせて以下の要件を満たすプログラムを作成してください。

  • 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.WaitGroupcontextを組み合わせ、複数のgoroutineの終了を管理し、メイン処理の待機を実現しました。

次節では、本記事の内容を簡潔にまとめ、学びを整理します。

まとめ

本記事では、Goのcontextパッケージを活用したgoroutineの制御とキャンセル処理について学びました。contextは並行処理を効率的に管理し、安全に制御するための重要なツールであり、特に以下の点が重要です:

重要ポイント

  1. contextの基本的な使い方
  • context.Background()context.TODO()で親contextを生成し、context.WithCancelcontext.WithTimeoutでキャンセルやタイムアウトを管理できます。
  1. goroutinecontextの連携
  • goroutine内でctx.Done()を監視し、キャンセルシグナルを受け取った際に処理を安全に終了させることができます。
  1. 複数のgoroutineを一括制御
  • contextを共有することで、複数のgoroutineを一括でキャンセルできます。また、sync.WaitGroupを組み合わせて、全てのgoroutineの終了を待つことが可能です。
  1. タイムアウトとキャンセル処理
  • context.WithTimeoutを利用して、時間制限内に処理を終わらせることができます。
  • context.WithCancelを用いて、親contextがキャンセルされた際に全ての子goroutineを一括で終了させることができます。
  1. エラー処理とトラブルシューティング
  • context deadline exceededcontext canceledといったエラーが発生する場合、適切なタイムアウト設定やキャンセル処理を行うことで防げます。

学習のまとめ


Goのcontextは、並行処理を扱う際に非常に強力なツールです。contextを適切に活用することで、プログラムの安定性や効率性を大幅に向上させることができます。この記事を通して、contextの使い方、エラーハンドリング、複数のgoroutineの制御方法を学びました。

今後、並行処理やキャンセル制御が求められるシステムにおいて、contextを積極的に活用し、スケーラブルで効率的なコードを作成できるようになるでしょう。

コメント

コメントする

目次
  1. Contextパッケージの基本概要
    1. Contextの主な役割
    2. 主要なContextの生成方法
    3. Contextの主要な型
  2. GoroutineとContextの関係性
    1. Goroutineの並行処理における課題
    2. Contextが解決する課題
    3. ContextとGoroutineの連携フロー
    4. 結果の説明
  3. Contextの生成と親子関係
    1. Contextの生成方法
    2. Contextの親子関係
    3. 親子関係のメリット
  4. キャンセル処理の実装例
    1. キャンセル処理の基本的な仕組み
    2. キャンセル処理の実装例
    3. コードの動作
    4. 出力結果
    5. 応用ポイント
  5. Contextによるタイムアウト設定
    1. タイムアウト設定の基本
    2. タイムアウトの実装例
    3. コードの動作
    4. 出力結果
    5. タイムアウト設定の応用
    6. 注意点
  6. Contextの値伝播機能
    1. Contextでの値の伝播
    2. 値伝播の実装例
    3. 出力結果
    4. 応用例
    5. 注意点
  7. Goroutineのキャンセル処理でのベストプラクティス
    1. 1. キャンセル処理を設計に組み込む
    2. 2. 必要に応じて早期キャンセルを行う
    3. 3. コンテキストを正しく解放する
    4. 4. 複数のGoroutineを効率的に管理する
    5. 5. エラー処理を忘れない
    6. まとめ
  8. 応用例:複数のGoroutineの制御
    1. 複数のGoroutineを一括キャンセルする例
    2. タイムアウトを適用したGoroutineの管理
    3. 複数のGoroutineをWaitGroupとContextで組み合わせる
    4. まとめ
  9. よくあるエラーとトラブルシューティング
    1. 1. `context deadline exceeded` エラー
    2. 2. `context canceled` エラー
    3. 3. キーの衝突による値取得の失敗
    4. 4. `cancel()`の呼び出し忘れ
    5. 5. タイムアウトとキャンセルの競合
    6. まとめ
  10. 演習問題と解答例
    1. 演習問題
    2. 解答例
    3. 解説
  11. まとめ
    1. 重要ポイント
    2. 学習のまとめ