Goroutine — RubyのThreadとは次元が違う軽量並行処理
goキーワードひとつで数万の並行タスクを実行。GILはない。
Rubyで並行処理を扱ったことがある人はGILの限界を知っているだろう。Threadを10個作ってもCPUバウンドの作業は1つしか実行されない。IOバウンドでのみ並行処理が意味を持つ。
Goにはこの問題がない。goroutineは本当に並列実行される。
goroutineの開始
go doSomething() // これだけ
RubyのThread.new { do_something }と似ているが、goroutineはスレッドではない。Goランタイムが管理する軽量「グリーンスレッド」に近い。メモリ使用量はスレッドの約1/100。goroutine 1つ約2KB、OSスレッドは約1MB。
GILがない
Ruby(CRuby)のGILは一度に1つのスレッドだけRubyコードを実行させる。IO待ち中にのみ他のスレッドが実行される。Goにはこの制約がない。goroutineがCPUコア数だけ本当に並列に実行される。
RubyでSidekiqのプロセスを複数立てて回避するパターンがある。Goではその必要がない。goroutineがマルチコアを自動で活用する。
問題:共有状態
RubyでMutexを使うように、Goにもsync.Mutexがある。ただしGoコミュニティの格言:「メモリを共有して通信するな、通信してメモリを共有せよ。」これがチャネル(Channel)だ。次の記事で扱う。
WaitGroup — RubyのThread#join
Rubyのthreads.each(&:join)のように、Goではsync.WaitGroupを使う:wg.Add(1) → go func() { defer wg.Done(); ... }() → wg.Wait()。
RubyからGoへ
Ruby: Thread.new { }(GILでCPU並列不可)→ Go: go func()(真の並列)
goroutineは約2KB、OSスレッドは約1MB — 数万個の同時実行可能
Ruby: threads.each(&:join) → Go: sync.WaitGroup(Add/Done/Wait)
Ruby: Mutex.new → Go: sync.Mutex(あるがChannel使用推奨)
メリット
- ✓ GILなしで真の並列 — CPUバウンド作業でRuby比圧倒的パフォーマンス
- ✓ goroutineが極めて軽いため並行モデル設計の自由度が高い
デメリット
- ✗ 競合状態(race condition)デバッグがRubyより難しい — go run -raceフラグ必須
- ✗ goroutineリーク — 終わらないgoroutineがメモリを食う問題