goroutine
| 対応: | Go 1.0(2012) |
|---|
goroutineはGoが提供する軽量スレッドです。『go』キーワードを関数呼び出しの前に付けるだけで、その関数を別の実行フローで並行実行できます。
構文
// 関数をgoroutineとして起動する
go 関数名(引数)
// 無名関数をgoroutineとして起動する
go func() {
// 並行して実行される処理
}()
goroutineは非常に軽量で、同時に数千〜数百万個起動できます。ただし『main()』が終了するとすべてのgoroutineも終了します。
goroutine の特徴
| 特徴 | 概要 |
|---|---|
| 軽量 | OSスレッドより大幅に軽量で、初期スタックサイズは数KB程度です。必要に応じて動的に拡張されます。 |
| Goランタイム管理 | GoのランタイムがgoroutineをOSスレッドにマッピングします。M個のgoroutineをN個のOSスレッドに効率的に割り当てるM:Nモデルを採用しています。 |
| 非同期実行 | 『go』で起動した関数はすぐに制御を返します。呼び出し元はgoroutineの完了を待ちません。 |
| 完了待ち | goroutineの完了を待つには、channelまたは『sync.WaitGroup』を使います。 |
| パニックの伝播 | goroutine内のpanicは呼び出し元には伝播しません。各goroutineで個別にrecover()する必要があります。 |
サンプルコード
goroutine.go
package main
import (
"fmt"
"sync"
"time"
)
// 処理を行う関数
func worker(id int, wg *sync.WaitGroup) {
defer wg.Done() // 処理終了時にWaitGroupに通知
fmt.Printf("Worker %d: 開始\n", id)
time.Sleep(time.Millisecond * 100) // 重い処理のシミュレーション
fmt.Printf("Worker %d: 完了\n", id)
}
func main() {
// 単純なgoroutineの起動
go fmt.Println("別のgoroutineから出力します") // 非同期で実行される
// WaitGroupでgoroutineの完了を待つ
var wg sync.WaitGroup
// 5つのgoroutineを起動する
for i := 1; i <= 5; i++ {
wg.Add(1) // goroutine起動前にカウントを増やす
go worker(i, &wg)
}
wg.Wait() // すべてのgoroutineが完了するまで待つ
fmt.Println("すべてのworkerが完了しました")
// 無名関数をgoroutineとして起動する
// ループ変数を引数で渡すことで値をキャプチャする
var wg2 sync.WaitGroup
results := make([]int, 5)
for i := 0; i < 5; i++ {
wg2.Add(1)
go func(idx int) {
defer wg2.Done()
results[idx] = idx * idx // 各goroutineが独立したインデックスを処理する
}(i) // ループ変数 i を引数として渡す
}
wg2.Wait()
fmt.Println("二乗の結果:", results) // 『[0 1 4 9 16]』と出力される
}
go run goroutine.go 別のgoroutineから出力します Worker 1: 開始 Worker 3: 開始 Worker 2: 開始 Worker 5: 開始 Worker 4: 開始 Worker 1: 完了 Worker 3: 完了 Worker 2: 完了 Worker 5: 完了 Worker 4: 完了 すべてのworkerが完了しました 二乗の結果: [0 1 4 9 16]
goroutineの実行順序は毎回異なる場合があります。上記の出力順は一例です。
channel を使った goroutine 間の通信
goroutine間でデータを受け渡すには『channel』を使います。Goの哲学は「共有メモリによる通信ではなく、通信によるメモリ共有」です。Mutexで共有変数を守るより、channelでデータを受け渡す設計の方がGoらしいとされています。
sample_goroutine_channel.go
package main
import (
"fmt"
"sync"
)
// ジョブをgoroutineで並列処理し、結果をchannelで受け取るパターン
func processJob(id int, input string, result chan<- string, wg *sync.WaitGroup) {
defer wg.Done()
result <- fmt.Sprintf("Job%d: %s processed", id, input)
}
func main() {
jobs := []string{"Kiryu", "Majima", "Saejima", "Akiyama", "Shinada"}
results := make(chan string, len(jobs)) // バッファ付きchannel
var wg sync.WaitGroup
for i, job := range jobs {
wg.Add(1)
go processJob(i+1, job, results, &wg)
}
// goroutineの完了後にchannelを閉じる
go func() {
wg.Wait()
close(results)
}()
// channelが閉じるまで結果を受け取る
for r := range results {
fmt.Println(r)
}
}
go run sample_goroutine_channel.go Job1: Kiryu processed Job3: Saejima processed Job2: Majima processed Job4: Akiyama processed Job5: Shinada processed
goroutineは並列に実行されるため、出力の順序は実行のたびに変わる可能性があります。
context によるキャンセル
長時間実行するgoroutineを外部からキャンセルしたい場合、『context.WithCancel』または『context.WithTimeout』を使います。Goroutineをキャンセルできるように設計しないと、プロセスが終了してもgoroutineがリークする「goroutineリーク」が発生します。
sample_goroutine_context.go
package main
import (
"context"
"fmt"
"time"
)
// contextを受け取り、キャンセルされたら終了するgoroutine
func monitor(ctx context.Context, name string) {
for {
select {
case <-ctx.Done():
fmt.Printf("%s: 監視を終了します(理由: %v)\n", name, ctx.Err())
return
default:
fmt.Printf("%s: 監視中...\n", name)
time.Sleep(100 * time.Millisecond)
}
}
}
func main() {
// 200ms後に自動キャンセルされるcontextを作成する
ctx, cancel := context.WithTimeout(context.Background(), 200*time.Millisecond)
defer cancel() // 必ずdeferでcancel()を呼び、リソースを解放する
go monitor(ctx, "Kiryu")
// contextがキャンセルされるまで待つ
<-ctx.Done()
fmt.Println("メイン: 処理を終了しました")
time.Sleep(100 * time.Millisecond) // goroutineの終了ログを待つ
}
go run sample_goroutine_context.go Kiryu: 監視中... Kiryu: 監視中... メイン: 処理を終了しました Kiryu: 監視を終了します(理由: context deadline exceeded)
CPU負荷などの実行環境によって「監視中...」の出力回数が前後する場合があります。
よくあるミス1: ループ変数のキャプチャ
ループ変数を直接クロージャでキャプチャすると、Go 1.22未満ではすべてのgoroutineが最終値を参照してしまいます。
package main
import "sync"
func main() {
var wg sync.WaitGroup
for i := 0; i < 3; i++ {
wg.Add(1)
go func() {
defer wg.Done()
// fmt.Println(i) // Go 1.22未満では全て「3」になる可能性があります
}()
}
wg.Wait()
}
OK: 引数で渡してコピーを作ります(Go 1.22未満での安全な方法)。
sample_goroutine_mistakes.go
package main
import (
"fmt"
"sync"
)
func main() {
var wg sync.WaitGroup
for i := 0; i < 3; i++ {
wg.Add(1)
go func(n int) {
defer wg.Done()
fmt.Printf("goroutine %d\n", n)
}(i) // i の値をコピーして渡す
}
wg.Wait()
}
go run sample_goroutine_mistakes.go goroutine 0 goroutine 1 goroutine 2
よくあるミス2: WaitGroupの値渡し
WaitGroup を値渡しするとコピーされてカウントがずれます。必ずポインタ(&wg)で渡してください。また goroutine の起動前に wg.Add(1) を呼ばないと、wg.Wait() が先に完了することがあります。
概要
goroutineはGoの並行処理の根幹です。OSスレッドと異なり非常に軽量なため、数千〜数百万のgoroutineを同時に実行できます。ただしgoroutine間でデータを共有する場合は同期処理が必要です。
ループ内でgoroutineを起動する場合、ループ変数を直接クロージャでキャプチャすると、すべてのgoroutineが同じ最終値を参照するバグが発生します。必ず引数として渡してください。
goroutineの完了を待つ方法として、『sync.WaitGroup』と『channel』があります。共有データへの安全なアクセスには『sync.Mutex』を使用してください。
記事の間違いや著作権の侵害等ございましたらお手数ですがこちらまでご連絡頂ければ幸いです。