概要
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でグローバル変数とローカル変数のアドレスを表示してみる。
分析
- 同じPID すべてのgoroutineは同じプロセス内で動作するため、PIDも同一である。
- グローバル変数の共有
globalVarのアドレスが全goroutineで同じである。 - ローカル変数の独立性
各goroutineは独立したスタックを持つが、この例では
localVarのアドレスを取得してfmt.Printfに渡しているため、エスケープ解析によってヒープに配置される。それでも各goroutineで異なるメモリ領域に配置されており、独立性は保たれている。 - データ競合
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ごとに独立 | プロセス全体で共有 |
| 速度 | 高速 | 相対的に遅い |
| 配置条件 | エスケープしない変数 | エスケープする変数 |
まとめ
- プロセスの独立性 子プロセスは親プロセスとは別の仮想アドレス空間を持ち、変数は共有されない。
- goroutineのメモリ共有 同一プロセス内で動作し、グローバル変数を共有するが、スタックは独立している。
- メモリ管理の特性 Goはエスケープ解析でスタックとヒープを自動的に振り分け、GCでヒープを管理する。