🚀

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へ

1

Ruby: Thread.new { }(GILでCPU並列不可)→ Go: go func()(真の並列)

2

goroutineは約2KB、OSスレッドは約1MB — 数万個の同時実行可能

3

Ruby: threads.each(&:join) → Go: sync.WaitGroup(Add/Done/Wait)

4

Ruby: Mutex.new → Go: sync.Mutex(あるがChannel使用推奨)

メリット

  • GILなしで真の並列 — CPUバウンド作業でRuby比圧倒的パフォーマンス
  • goroutineが極めて軽いため並行モデル設計の自由度が高い

デメリット

  • 競合状態(race condition)デバッグがRubyより難しい — go run -raceフラグ必須
  • goroutineリーク — 終わらないgoroutineがメモリを食う問題

ユースケース

Ruby Sidekiqワーカーをgoに転換してプロセス数を減らしたい時 数千の同時HTTPリクエストを処理するサーバーが必要な時(Rubyはプロセス/スレッド制限)