Goでプロセス・goroutine・メモリを覗いてみた

概要

Goを使って、プロセスのアドレス空間・goroutineの動作・スタックとヒープを軽く覗いてみる。

子プロセスとアドレス空間の違いを確認する

Goでは標準的にos/execパッケージを使って新しいプロセスを起動する。 os/execは内部的にUnix系OSではfork()exec()相当の処理を行い、新しいプログラム(この例では自分自身)を実行する。

親子プロセスで同じ変数のアドレスを表示し、メモリ空間の独立性を確認してみる。

実行結果の意味

  • プロセス分離: 親と子で異なるPIDが表示される。

  • アドレス空間の独立性:

    • グローバル変数globalVarもローカル変数localVarも、親と子で異なるアドレスが表示される。
    • これは、Unix系OSがプロセスごとに独立した仮想アドレス空間を割り当てるためである。 数値が似て見える場合もあるが、物理メモリ上では完全に別物である。

プロセス間のメモリ空間図解

graph TD A[親プロセス<br/>PID=1234] -->|os/exec| B[子プロセス<br/>PID=1235] subgraph Astack[親プロセスのメモリ空間] A1[スタック: localVar=10] A2[ヒープ: globalVar=100] end subgraph Bstack[子プロセスのメモリ空間] B1[スタック: localVar=20] B2[ヒープ: globalVar=100] end style Astack fill:#d1e8ff,stroke:#333,stroke-width:1px style Bstack fill:#ffd1d1,stroke:#333,stroke-width:1px

goroutineとメモリ共有の観察

Goの軽量スレッドであるgoroutineを使用してメモリを観察する。goroutineは同一プロセス内で動作するため、仮想アドレス空間を共有する。 これを確かめるため、複数のgoroutineでグローバル変数とローカル変数のアドレスを表示してみる。

分析

  1. 同じPID すべてのgoroutineは同じプロセス内で動作するため、PIDも同一である。
  2. グローバル変数の共有 globalVarのアドレスが全goroutineで同じである。
  3. ローカル変数の独立性 各goroutineは独立したスタックを持つが、この例ではlocalVarのアドレスを取得してfmt.Printfに渡しているため、エスケープ解析によってヒープに配置される。それでも各goroutineで異なるメモリ領域に配置されており、独立性は保たれている。
  4. データ競合 globalVarの値が106になったのはたまたまで、実行タイミングによって結果は変わる。 安全に並行処理を行うには、チャネルやミューテックスなどの同期機構が必要である。 → go run -raceで競合検出可能。

goroutine間のメモリ共有図解

graph TD subgraph Proc[単一プロセス<br/>PID=2000] subgraph G0[g0のスタック] G0L[localVar=5] end subgraph G1[g1のスタック] G1L[localVar=10] end Heap[ヒープ領域<br/>globalVar=100] end G0 --- Heap G1 --- Heap style Proc fill:#f0f0f0,stroke:#333,stroke-width:1px style G0 fill:#d1e8ff,stroke:#333,stroke-width:1px style G1 fill:#ffd1d1,stroke:#333,stroke-width:1px style Heap fill:#d1ffd1,stroke:#333,stroke-width:1px

goroutineとチャネルによる安全な並行処理

チャネルを使えば、共有変数を直接更新せずにデータをやり取りできる。

ヒープ領域とガベージコレクション

Goでは動的メモリはヒープに置かれることが多いが、配置先はエスケープ解析で確認できる。

  • ヒープ領域はGCによって管理され、明示的に解放する必要はない。

スタックとヒープの違い

項目 スタック ヒープ
管理方法 関数呼び出しに伴い自動で確保・解放 GCが管理
領域の独立性 goroutineごとに独立 プロセス全体で共有
速度 高速 相対的に遅い
配置条件 エスケープしない変数 エスケープする変数

まとめ

  1. プロセスの独立性 子プロセスは親プロセスとは別の仮想アドレス空間を持ち、変数は共有されない。
  2. goroutineのメモリ共有 同一プロセス内で動作し、グローバル変数を共有するが、スタックは独立している。
  3. メモリ管理の特性 Goはエスケープ解析でスタックとヒープを自動的に振り分け、GCでヒープを管理する。