Goの並行・並列処理モデルとgoroutineスケジューリング

Goの並行・並列処理モデルとgoroutineスケジューリング

Read in: en
Goの並行・並列処理モデルとgoroutineスケジューリング

概要

Go言語は、軽量なgoroutineとランタイム機構により並行処理(concurrency)を強力にサポートし、Go 1.5以降はデフォルトでGOMAXPROCSが利用可能CPUコア数に設定されるため、適切に設定することでマルチコアを活かした並列実行(parallelism)も可能とする。本記事では、goroutineのスケジューリングやCPUバウンド処理でのマルチコア活用の仕組み、OSプロセス・スレッド・goroutineの関係を整理する。

並行(concurrency)と並列(parallelism)の違い

Goで直接指示して実装できるのは主に並行処理(concurrency)であり、goroutineにより複数タスクを重ね合わせて扱う。真の並列実行(parallelism)を行うには、実行環境が複数のCPUコアを持ち、かつGOMAXPROCSを2以上に設定する必要がある。

並行(concurrency)の時間軸視点

単一コア上でタスクが時間を分割して重ね合わせて実行される様子を表す。実際のスケジューラはプリエンプションポイントや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: 中断

並列(parallelism)の時間軸視点

複数コア上でタスクが物理的に同時実行される様子。実行環境が複数コアかつ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

goroutineとは

M-P-Gモデル(Machine, Processor, Goroutine)

Goランタイムのコア概念:

G → P → M の流れ

  1. goroutine生成時、Pのローカルキューまたはグローバルキューに登録。
  2. 空きのMがPを取得し、キューからgoroutineを取り出して実行。
  3. 終了またはI/O待ち・プリエンプションで中断後、別の実行可能goroutineが同様に実行される。

goroutineは軽量に生成・中断・再開され、高い並行性と並列性を実現するが、大量に生成するとスケジューリングオーバーヘッドやスタック成長コストが発生する可能性があるため、適切な粒度設計とプロファイリングによる検証が重要である。

M-P-Gモデルの深掘りと参考記事

これらを参照し、図解や具体的コード例を交えて説明すると理解が深まる。

GOMAXPROCSと並列実行

スケジューリングの詳細

ランナブルキューとワークステーリング

プリエンプティブスケジューリング

ブロッキング操作時の挙動

システムコールとスレッド管理

スタック管理とメモリ効率

CPUバウンド処理と並列活用

I/Oバウンド処理との親和性

スケジューラチューニング

プリエンプションの仕組み(Go 1.14以降)

func busyLoop() {
    for i := 0; i < 1e9; i++ {
        // ループ内の計算処理
        _ = i * i
        // Goランタイムはこのあたりでプリエンプションポイントを挿入し、他のgoroutine実行を許可することがある
    }
}

実際のプリエンプションはランタイム内部で自動的に行われ、明示的に記述する必要はないが、上記のようにループや関数呼び出しが安全点となり得ることを理解しておくと、長時間CPUを占有する処理でも並行性を維持しやすいことがわかる。

OSプロセス・スレッド・goroutineの関係

[OSプロセス]
    ├─ Goランタイム起動 → 複数のOSスレッド(M)生成・管理
    ├─ Goランタイム内にPを複数(GOMAXPROCS分)用意
    └─ goroutine(G)はユーザーレベルで生成され、Pのランナブルキューに置かれる
       └─ 空きのMがPを取得すると、キューからGを取り出して実行

高い並行性・並列性を同時に実現する仕組みを理解し、ベンチマークやプロファイリングを通じて性能最適化に役立てる。

まとめ

GoランタイムはM-P-Gモデルを基盤に、軽量なgoroutine生成と高度なスケジューリング機構を提供し、並行処理と並列処理を明確に区別しつつ自然にサポートする。開発者はGOMAXPROCS設定、goroutine粒度、プロファイリング、同期手法などを理解することで、性能最適化やスループット向上を図ることができる。

参考

Tags: Golang
Share: 𝕏 Post Facebook Hatena
✏️ View source / Discuss on GitHub
☕ サポート

このブログを応援していただける方は、以下からサポートをお願いします。いただいたサポートはブログ運営・技術研鑽に活用します。


関連記事