PR

Go言語(Golang)のgoroutine(ゴルーチン/ゴールーチン)の使い所まとめ

基礎

こんにちは。Tomoyuki(@tomoyuki65)です。

Go言語(Golang)を使うメリットの一つとしては、goroutine(ゴルーチン/ゴールーチン)による非同期処理によって並行処理が簡単に行えることです。

ただし、使い所は選ぶ必要があるため、どういった場面で使えばいいかの判断基準は押さえておく必要があります。

この記事では、そんなgoroutine(ゴルーチン/ゴールーチン)の使い所についてまとめます。

 

【関連記事】

Go言語(Golang)はgoroutine(ゴールーチン)で並行処理が可能!
こんにちは。Tomoyuki(@tomoyuki65)です。Go言語(Golang)ではgoroutine(ゴールーチン)を使うことによって並行処理が可能なため、上手く使えばパフォーマンス改善に繋がります!※プログラムは通常一行ずつ処理をし...
Go言語(Golang)のchannel(チャネル)型でgoroutine(ゴールーチン)間のデータを送受信!
こんにちは。Tomoyuki(@tomoyuki65)です。Go言語(Golang)にはchannel(チャネル)型というのがあり、これを使うことでgoroutine(ゴールーチン)間のデータを送受信できます。Go言語(Golang)のch...
Go言語(Golang)のmutex(ミューテックス)でgoroutine(ゴールーチン)の排他制御を知る!
こんにちは。Tomoyuki(@tomoyuki65)です。Go言語(Golang)でgoroutine(ゴールーチン)から共有リソースに対して読み書きをする必要がある場合、何も考慮しなければ競合状態になってエラーが発生する可能性があるので...

 

Go言語(Golang)のgoroutine(ゴルーチン/ゴールーチン)の使い所まとめ

まずgoroutine(ゴルーチン/ゴールーチン)が有効な場面としては、処理の待ち時間(I/O待ち)が発生する場面や、独立して実行できる複数のタスクがある場面になります。

goroutineの主な使用場面としては以下の通りです。

・ネットワークI/O処理
・ファイルI/O処理
・時間のかかる独立した計算処理
・バックグラウンドタスク

 

ネットワークI/O処理

最も一般的な使用場面としては、ネットワークI/O処理がある場面です。

ネットワーク通信(DB接続処理、外部APIの実行など)は応答が帰ってくるまでに時間がかかり、その間にメインの処理が停止(ブロック)してしまうため、goroutineによる非同期処理が有効です。

例えばDBからn回データを取得するような場合や、複数の外部APIからデータを取得したい場合、goroutineを使って並行処理をすることで、スループット(単位時間あたりにシステムが処理できるデータ量や、実行できる処理能力)が向上します。

 

ファイルI/O処理

ファイルI/O処理(ファイルの読み書き)についてもディスクアクセスが絡むため、待ち時間が発生しやすい処理になります。

 

時間のかかる独立した計算処理

タスク間で依存関係が無く、それぞれが独立して完了できるような計算処理の場合も、goroutineを使うことを検討しましょう。

ただし、CPUでボトルネックが出やすい処理でgoroutineを増やしすぎるとオーバーヘッド(余計な負荷など)が増えるため、同時実行させるgoroutineの数の制限は必要です。

 

バックグラウンドタスク

メイン処理を妨げずに裏側で実行したいようなバックグラウンドタスクについても、goroutineを使うことを検討しましょう。

 

goroutine(ゴルーチン/ゴールーチン)の使用判断基準

項目 検討基準(判断ポイント) 使うべき場面(Yes) 使わない方が良い場面(No)
I/O待ち 処理中にネットワークディスクからの応答待ち(ブロッキング)が頻繁に発生するか? 🌐 ネットワーク通信 (API、DB、HTTPリクエストなど) や ファイルI/O が多いタスク。 処理が非常に短時間で、I/O待ちが全く発生しないCPUのみを使うタスク。
タスクの独立性 処理が他の処理の完了を待つ必要がなく、並行して実行できるか? 📤 複数の独立したタスクを同時に進めたい場合 (例: 複数のデータを並列で取得・処理)。 厳密な順序性が求められ、並行化すると同期処理が複雑になる場合。
処理時間 処理の完了に時間がかかり、メインの実行フローを遅延させているか? ⏱️ 実行時間の長いタスクをバックグラウンドで非同期実行し、メイン処理をブロックしたくない場合。 処理が非常に短く、ゴルーチン生成のオーバーヘッドがメリットを上回る場合。
リソース CPUコアを有効活用し、スループットを向上させたいか? 💻 マルチコアCPUをフル活用し、システム全体の処理能力を上げたいサーバー処理など。 シングルスレッドで十分な単純なタスクや、処理能力向上が不要な場合。

 

channel(チャネル)の使用判断基準

goroutineを使うと判断した後、goroutine間で安全にデータをやり取りする必要があったり、複雑な同期が必要な場合は、channel(チャネル)を使うことになります。

その際はまずchannelを使った通信による解決を検討し、目的が単純な完了待ちや排他ロックであればsync.WaitGroupやsync.Mutexの使用を検討するようにして下さい。

目的 推奨される方法 補足
データの送受信 チャネル Goの哲学に沿った最も一般的な方法。
複数のゴルーチンの完了待ち sync.WaitGroup データをやり取りする必要がなければ、最もシンプル。
イベント通知・シグナリング チャネル closeによるブロードキャストなど柔軟な同期が可能。
共有リソースへの排他制御 sync.Mutex または チャネル Mutexは直接的でシンプル。 チャネルはアクセスを管理するゴルーチンを作る設計で対応可能。
複雑な同期・協調動作 チャネル (select文と併用) 複数のイベントを待つなど高度な制御に適している。

 

サンプルコード「データの送受信」

目的:あるゴルーチンで生成したデータを、別の処理やゴルーチンなどに渡す。

package main

import (
    "fmt"
    "time"
)

// チャネルへのメッセージ送信処理
func sendMessage(ch chan<- string, count int) {
    for i := 1; i <= count; i++ {
        // 送信メッセージを生成
        msg := fmt.Sprintf("メッセージ %d", i)

        // チャネルにメッセージを送信
        ch <- msg

        // 待機処理
        time.Sleep(500 * time.Millisecond)
    }

    // チャネルをクローズ
    close(ch)
}

func main() {
    // string型のデータを送受信するチャネルを作成
    textChan := make(chan string)

    // メッセージ送信回数
    count := 3

    // ゴルーチンを実行してメッセージ送信
    go sendMessage(textChan, count)

    // チャネルがクローズするまで受信処理
    fmt.Println("受信待機中...")
    for msg := range textChan {
        fmt.Printf("受信: %s\n", msg)
    }

    fmt.Println("処理完了")
}

※rangeを使ったチャネルの受信処理ではチャネルが閉じられるまでループが終わらないため、チャネルのクローズ処理が必要になります。

 

サンプルコード「複数のゴルーチンの完了待ち」

目的:データの受け渡しは不要で、単に複数のゴルーチンが全て完了するのを待ちたい。

package main

import (
    "fmt"
    "sync"
    "time"
)

func worker(id int, wg *sync.WaitGroup) {
    // 処理完了後にwg.Done()を呼んで完了させる
    defer wg.Done()

    fmt.Printf("ワーカー %d: 処理開始\n", id)

    // 待機処理
    time.Sleep(1 * time.Second)

    fmt.Printf("ワーカー %d: 処理完了\n", id)
}

func main() {
    // WaitGroupの定義
    var wg sync.WaitGroup

    // ワーカー数の定義
    numWorkers := 3

    // ワーカー関数を実行
    for i := 1; i <= numWorkers; i++ {
        // WaitGroupのカウンターを一つ増やす
        wg.Add(1)

        // ワーカーをゴルーチンで非同期実行
        go worker(i, &wg)
    }

    fmt.Println("全てのワーカーの完了待ち...")
    wg.Wait()

    fmt.Println("全てのワーカー処理が完了しました。")
}

 

サンプルコード「イベント通知・シグナリング」

目的:ある処理が完了したことを、他の処理やゴルーチンに通知する。

package main

import (
    "fmt"
    "time"
)

// 何らかの重いタスクの処理
func heavyTask(done chan<- bool) {
    fmt.Println("タスクを開始...")

    // 待機処理
    time.Sleep(3 * time.Second)

    fmt.Println("タスク完了")
    // 完了通知としてチャネルに値を送信
    done <- true
}

func main() {
    // 通知用チャネルの定義
    doneChan := make(chan bool)

    // ゴルーチンによりタスク実行
    go heavyTask(doneChan)

    fmt.Println("タスクの完了通知を待っています...")
    <-doneChan

    fmt.Println("メイン処理を終了します。")
}

 

サンプルコード「共有リソースへの排他制御」

目的:複数のゴルーチンから同じ変数にアクセスさせたい場合、競合状態が発生するので保護する。

 

・sync.Mutexを使う場合

package main

import (
    "fmt"
    "sync"
)

func main() {
    // カウンターの定義
    var counter int

    // WaitGroupの定義
    var wg sync.WaitGroup

    // ミューテックスの定義
    var mu sync.Mutex

    // ゴルーチンの回数設定
    numGoroutines := 100
    wg.Add(numGoroutines)

    // ゴルーチンを実行
    for i := 0; i < numGoroutines; i++ {
      // 即時関数で実行する
      go func() {
          defer wg.Done()

          // カウンターへのロック
          mu.Lock()
          counter++
          // アンロック
          mu.Unlock()
      }()
    }

    // ゴルーチンの完了待ち
    wg.Wait()

    // カウンターの値がゴルーチンの回数設定と同じ値になること
    fmt.Printf("最終カウンター: %d\n", counter)
}

 

・チャネルを使う場合(より複雑な制御)

package main

import (
    "fmt"
    "sync"
)

// カウンター管理用の関数
func counterManager(requests <-chan int, wg *sync.WaitGroup) {
    defer wg.Done()

    // カウンターの定義
    var counter int

    // チャネルがクローズするまで受信処理
    for range requests {
        counter++
    }

    fmt.Printf("最終カウンター: %d\n", counter)
}

func main() {
    // リクエスト用のチャネルを定義
    requests := make(chan int)

    // カウンター管理用のWaitGroupの定義
    var wgCM sync.WaitGroup

    // ゴルーチン用のWaitGroupの定義
    var wgG sync.WaitGroup

    // カウンター管理用のゴルーチンを起動
    wgCM.Add(1)
    go counterManager(requests, &wgCM)

    // ゴルーチンの回数設定
    numGoroutines := 100
    wgG.Add(numGoroutines)

    // ゴルーチンを実行
    for i := 0; i < numGoroutines; i++ {
      // 即時関数で実行する
      go func() {
          defer wgG.Done()

          // カウンターにリクエストを送信
          requests <- 1
      }()
    }

    // ゴルーチンの完了待ち
    wgG.Wait()

    // リクエスト用のチャネルをクローズ
    close(requests)

    // カウンター管理用ゴルーチンの完了待ち
    wgCM.Wait()
}

※リソースへのアクセスを単一のゴルーチンに限定し、他のゴルーチンはその単一ゴルーチンにチャネル経由でリクエストを送る設計です。

 

サンプルコード「複雑な同期・協調動作」

目的:複数のチャネルのいずれかが実行されるのを待ち、実行されたものに応じて対象の処理を実行させるか、タイムアウト処理を実行する。

package main

import (
    "fmt"
    "time"
)

// タスク実行処理(戻り値に受信用のチャネルを返す)
func task(name string, delay time.Duration) <-chan string {
    // 戻り値用のチャネルを定義
    resultChan := make(chan string)

    // ゴルーチンを実行
    go func() {
        // 待機処理
        time.Sleep(delay)

        // チャネルにテキストを送信
        resultChan <- fmt.Sprintf("%sが完了しました!", name)
    }()

    // 戻り値にチャネルを返す
    return resultChan
}

func main() {
    // 2つのタスクを並行で実行
    result1 := task("タスクA", 5*time.Second)
    result2 := task("タスクB", 2*time.Second)

    fmt.Println("タスク完了待ち...")

    // チャネル用のselect文を用いた待機処理
    // 最初に受信した結果に応じた処理を実行するか、タイムアウト処理を実行する
    select {
    case res := <-result1:
        fmt.Println(res)
    case res := <-result2:
        fmt.Println(res)
    case <-time.After(10 * time.Second):
        fmt.Println("タイムアウトしました!")
    }  
}

※select文を使って複数のイベントを待つような処理を書ける。

 

スポンサーリンク

最後に

今回はGo言語(Golang)のgoroutine(ゴルーチン/ゴールーチン)の使い所についてまとめました。

goroutineは便利ですが、色々な使い方があって使い所の理解も必要になるため、使い慣れていない方はぜひ参考にしてみて下さい。

 

この記事を書いた人
Tomoyuki

SE→ブロガーを経て、現在はWeb系エンジニアをしています!

Tomoyukiをフォローする
基礎
スポンサーリンク
Tomoyukiをフォローする

コメント

タイトルとURLをコピーしました