Go言語の並行処理入門:goroutineとchannelの使い方を完全解説

Go言語の並行処理入門:goroutineとchannelの使い方を完全解説 | mohablog

Go言語は並行処理を非常に簡単に扱える言語として知られています。その核となるのがgoroutinechannelです。本記事では、これらの基本的な使い方から実践的な例まで、わかりやすく解説します。Go言語で効率的で高速な並行プログラムを書きたい方は、ぜひご一読ください。

目次

goroutineとは何か

goroutineは、Go言語が提供する軽量なスレッドのようなものです。通常のスレッドと比較すると、メモリ使用量が少なく、起動が非常に高速です。Go言語のランタイムは複数のgoroutineを効率的にスケジュールするため、数千から数百万のgoroutineでも扱うことができます。

  • メモリ効率が高い(スレッドの1000倍以上軽量)
  • 起動と管理が簡単
  • ランタイムが自動的にスケジューリング
  • 複数のCPUコアを活用可能

goroutineの基本的な使い方

goroutineはgoキーワードを関数の前に付けるだけで起動できます。以下の例を見てみましょう。

package main

import (
    "fmt"
    "time"
)

func printMessage(msg string) {
    for i := 0; i < 5; i++ {
        fmt.Println(msg, i)
        time.Sleep(time.Second)
    }
}

func main() {
    go printMessage("Goroutine 1")
    go printMessage("Goroutine 2")
    
    // メインのgoroutineが終了しないように待機
    time.Sleep(6 * time.Second)
}

このコードは2つのgoroutineを並行して実行します。goキーワードを付けた関数呼び出しは、バックグラウンドで実行され、メインプログラムはすぐに次の行へ進みます。

channelについて理解する

channelは、goroutine間でデータを安全に受け渡すための仕組みです。channelを使うことで、複数のgoroutine間での同期と通信が簡潔に実現できます。

「Do not communicate by sharing memory; instead, share memory by communicating.」(メモリを共有することで通信するのではなく、通信することでメモリを共有しなさい)- Go言語の設計思想

channelの基本的な使い方

channelはmake関数で作成し、<-演算子でデータを送受信します。

package main

import "fmt"

func main() {
    // int型のchannelを作成
    ch := make(chan int)
    
    // goroutineでchannelにデータを送信
    go func() {
        ch <- 42  // 42をchannelに送信
    }()
    
    // channelからデータを受信
    value := <-ch
    fmt.Println("受信した値:", value)
}

このコードでは、goroutine内で値をchannelに送信し、メインgoroutineで受信しています。channelは送受信が完了するまでブロックするため、自動的に同期が取られます。

バッファ付きchannelの活用

デフォルトのchannelはバッファがない(バッファサイズが0)ため、送受信が同時に起こらないとデッドロックになります。バッファを持たせることで、複数の値を蓄積できます。

package main

import "fmt"

func main() {
    // バッファサイズ3のchannelを作成
    ch := make(chan string, 3)
    
    // 3つの値を送信(ブロックしない)
    ch <- "apple"
    ch <- "banana"
    ch <- "cherry"
    
    // 3つの値を受信
    fmt.Println(<-ch)  // apple
    fmt.Println(<-ch)  // banana
    fmt.Println(<-ch)  // cherry
}

バッファ付きchannelは、送信元と受信元のタイミングがずれている場合に便利です。

複数のgoroutineと並行処理の実例

より実践的な例として、複数のworker goroutineがタスクを処理する場面を考えてみます。

package main

import (
    "fmt"
    "time"
)

func worker(id int, jobs <-chan int, results chan<- int) {
    for job := range jobs {
        fmt.Printf("Worker %d が job %d を処理中\n", id, job)
        time.Sleep(time.Second)
        results <- job * 2
    }
}

func main() {
    jobs := make(chan int, 5)
    results := make(chan int, 5)
    
    // 3つのworkerを起動
    for w := 1; w <= 3; w++ {
        go worker(w, jobs, results)
    }
    
    // 5つのjobを投入
    for j := 1; j <= 5; j++ {
        jobs <- j
    }
    close(jobs)  // jobsのchannelを閉じる
    
    // 結果を受け取る
    for r := 1; r <= 5; r++ {
        fmt.Println("結果:", <-results)
    }
}

このコードでは、3つのworkerが同時に複数のタスクを処理しています。close()関数でchannelを閉じることで、rangeループを適切に終了させることができます。

select文による複数channelの制御

複数のchannelを同時に監視する必要がある場合、select文を使用します。

package main

import (
    "fmt"
    "time"
)

func main() {
    ch1 := make(chan string)
    ch2 := make(chan string)
    
    go func() {
        time.Sleep(1 * time.Second)
        ch1 <- "メッセージ1"
    }()
    
    go func() {
        time.Sleep(2 * time.Second)
        ch2 <- "メッセージ2"
    }()
    
    // 2つのchannelのうち、先に値が来た方を受信
    for i := 0; i < 2; i++ {
        select {
        case msg1 := <-ch1:
            fmt.Println("受信:", msg1)
        case msg2 := <-ch2:
            fmt.Println("受信:", msg2)
        }
    }
}

select文は複数のchannelの受信を監視し、いずれかが準備できた時点で実行します。タイムアウト処理やデフォルトケースも組み合わせて使用できます。

channelのクローズとrange

channelをclose()で閉じると、受信側で値がなくなったことを検出できます。

package main

import "fmt"

func main() {
    ch := make(chan int)
    
    go func() {
        for i := 1; i <= 3; i++ {
            ch <- i
        }
        close(ch)  // channelをクローズ
    }()
    
    // rangeでchannelから値を受け取る
    for value := range ch {
        fmt.Println(value)
    }
    fmt.Println("完了")
}

rangeループはchannelが閉じられると自動的に終了します。これはタスク完了の通知を待つ場合に非常に便利です。

注意点とベストプラクティス

  • デッドロックに注意:バッファなしchannelで送信側と受信側が揃わないと、プログラムがハング
  • クローズ済みchannelへの送信はパニック:必ず送信側でchannelをクローズしましょう
  • 受信側でのクローズは避ける:送信元が複数の場合、クローズがエラーになる
  • goroutineの生存期間管理:メインgoroutineが終了する前に全goroutineが完了することを確認
  • syncパッケージの活用:WaitGroupを使ってgoroutineの完了を待つと安全

WaitGroupを使った安全な並行処理

複数のgoroutineの完了を確実に待つには、sync.WaitGroupを使うのが推奨です。

package main

import (
    "fmt"
    "sync"
)

func main() {
    var wg sync.WaitGroup
    results := make(chan int)
    
    // 3つのgoroutineを登録
    for i := 1; i <= 3; i++ {
        wg.Add(1)
        go func(id int) {
            defer wg.Done()  // goroutine完了時に呼ばれる
            results <- id * 10
        }(i)
    }
    
    // 結果を受け取りながら並行実行
    go func() {
        wg.Wait()  // 全goroutineの完了を待つ
        close(results)
    }()
    
    for result := range results {
        fmt.Println("結果:", result)
    }
}

WaitGroupを使うことで、全goroutineの完了を確実に確認でき、プログラムが途中で終了するリスクを回避できます。

まとめ

  • goroutinegoキーワード一つで簡単に並行処理を実現できる軽量スレッド
  • channelはgoroutine間での安全なデータ受け渡しを可能にする通信機構
  • バッファなしchannelは送受信が同期し、バッファ付きchannelは複数値を蓄積可能
  • select文で複数channelの監視や、タイムアウト処理が実現できる
  • sync.WaitGroupを使うことで、全goroutineの完了を安全に待つことができる
  • channelのクローズやデッドロック対策など、エラーハンドリングに注意が必要
  • Go言語の並行処理は設計思想に基づき、メモリ共有ではなく通信でデータを共有する
よかったらシェアしてね!
  • URLをコピーしました!
  • URLをコピーしました!
目次