Go言語は、軽量なgoroutineとランタイム機構により並行処理(concurrency)を強力にサポートし、Go 1.5以降はデフォルトでGOMAXPROCSが利用可能CPUコア数に設定されるため、適切に設定することでマルチコアを活かした並列実行(parallelism)も可能とする。本記事では、goroutineのスケジューリングやCPUバウンド処理でのマルチコア活用の仕組み、OSプロセス・スレッド・goroutineの関係を整理する。
Goで直接指示して実装できるのは主に並行処理(concurrency)であり、goroutineにより複数タスクを重ね合わせて扱う。真の並列実行(parallelism)を行うには、実行環境が複数のCPUコアを持ち、かつGOMAXPROCS
を2以上に設定する必要がある。
単一コア上でタスクが時間を分割して重ね合わせて実行される様子を表す。実際のスケジューラはプリエンプションポイントやI/O完了通知のタイミングで切り替えを行うため、切り替えタイミングは厳密に決定的ではないが、goroutineはI/O待ちやランタイムプリエンプションで動的に切り替わりながら動作する。
sequenceDiagram
participant Core as コア
participant TaskA as タスクA
participant TaskB as タスクB
Note over Core: 並行(concurrency)
TaskA->>Core: 実行時間スライス1
Note right of Core: タスクA動作
Core-->>TaskA: 中断(I/O待ちやプリエンプション)
TaskB->>Core: 実行時間スライス1
Note right of Core: タスクB動作
Core-->>TaskB: 中断(I/O待ちやプリエンプション)
TaskA->>Core: 実行時間スライス2
Note right of Core: タスクA再開
Core-->>TaskA: 中断
TaskB->>Core: 実行時間スライス2
Note right of Core: タスクB再開
Core-->>TaskB: 中断
複数コア上でタスクが物理的に同時実行される様子。実行環境が複数コアかつGOMAXPROCS
が2以上の場合に可能となる。
sequenceDiagram
participant Core1 as コア1
participant Core2 as コア2
participant TaskA as タスクA
participant TaskB as タスクB
Note over Core1,Core2: 並列(parallelism)
par タスクの同時実行
TaskA->>Core1: 同時実行
and
TaskB->>Core2: 同時実行
end
軽量な実行単位: 通常のOSスレッドより小さい初期スタック(数KB)から始まり、必要に応じて伸長・縮小。大量生成してもオーバーヘッドが小さい。
生成方法:
go func() {
// 並行で実行したい処理
}()
返り値は直接取得できず、通信にはチャネルや同期プリミティブを使用。
ランタイム制御: 実行タイミングやOSスレッドへの割り当てはGoランタイムスケジューラが担当。
Goランタイムのコア概念:
runtime.GOMAXPROCS
で設定可能(通常はCPUコア数と同じ)。goroutineは軽量に生成・中断・再開され、高い並行性と並列性を実現するが、大量に生成するとスケジューリングオーバーヘッドやスタック成長コストが発生する可能性があるため、適切な粒度設計とプロファイリングによる検証が重要である。
これらを参照し、図解や具体的コード例を交えて説明すると理解が深まる。
runtime.GOMAXPROCS(n)
でP数を設定する。Go 1.5以降はデフォルトで利用可能CPUコア数が設定されるが、以前のバージョンではデフォルトが1であった。環境変数GOMAXPROCS
でも明示的に設定でき、OSスレッド利用の挙動にも影響を与える。CPUバウンド処理を複数のgoroutineに分割し、GOMAXPROCS
を適切に設定することで、Goランタイムが複数OSスレッド上で並列実行しマルチコアを活かす。
分割や結果結合のオーバーヘッド、同期コストに留意し、適切な粒度で設計する。
例:
runtime.GOMAXPROCS(4)
var wg sync.WaitGroup
for i := 0; i < 4; i++ {
wg.Add(1)
go func(id int) {
defer wg.Done()
heavyComputation(id)
}(i)
}
wg.Wait()
go tool pprof
などでCPUプロファイルやスケジュール待ち時間を分析し、ボトルネックを特定する。GODEBUG=schedtrace=1000,scheddetail=1
でスケジューラ挙動をログ化し、負荷テスト時の振る舞いを観察する。func busyLoop() {
for i := 0; i < 1e9; i++ {
// ループ内の計算処理
_ = i * i
// Goランタイムはこのあたりでプリエンプションポイントを挿入し、他のgoroutine実行を許可することがある
}
}
実際のプリエンプションはランタイム内部で自動的に行われ、明示的に記述する必要はないが、上記のようにループや関数呼び出しが安全点となり得ることを理解しておくと、長時間CPUを占有する処理でも並行性を維持しやすいことがわかる。
GOMAXPROCS
)が同時並列実行可能goroutine数を決定する。[OSプロセス]
├─ Goランタイム起動 → 複数のOSスレッド(M)生成・管理
├─ Goランタイム内にPを複数(GOMAXPROCS分)用意
└─ goroutine(G)はユーザーレベルで生成され、Pのランナブルキューに置かれる
└─ 空きのMがPを取得すると、キューからGを取り出して実行
高い並行性・並列性を同時に実現する仕組みを理解し、ベンチマークやプロファイリングを通じて性能最適化に役立てる。
GoランタイムはM-P-Gモデルを基盤に、軽量なgoroutine生成と高度なスケジューリング機構を提供し、並行処理と並列処理を明確に区別しつつ自然にサポートする。開発者はGOMAXPROCS
設定、goroutine粒度、プロファイリング、同期手法などを理解することで、性能最適化やスループット向上を図ることができる。
関連書籍