Goを使って、プロセスのアドレス空間・goroutineの動作・スタックとヒープを軽く覗いてみる。
Goでは標準的にos/exec
パッケージを使って新しいプロセスを起動する。
os/exec
は内部的にUnix系OSではfork()
+exec()
相当の処理を行い、新しいプログラム(この例では自分自身)を実行する。
親子プロセスで同じ変数のアドレスを表示し、メモリ空間の独立性を確認してみる。
package main
import (
"fmt"
"os"
"os/exec"
)
var globalVar = 100
func main() {
localVar := 10
fmt.Printf("Parent: PID=%d, globalVar=%p, localVar=%p\n",
os.Getpid(), &globalVar, &localVar)
cmd := exec.Command(os.Args[0], "child")
cmd.Stdout = os.Stdout
cmd.Run()
}
func init() {
if len(os.Args) > 1 && os.Args[1] == "child" {
localVar := 20
fmt.Printf("Child: PID=%d, globalVar=%p, localVar=%p\n",
os.Getpid(), &globalVar, &localVar)
os.Exit(0)
}
}
Parent: PID=15224, globalVar=0x100720448, localVar=0x14000102020
Child: PID=15225, globalVar=0x10502c448, localVar=0x14000090020
プロセス分離: 親と子で異なるPIDが表示される。
アドレス空間の独立性:
globalVar
もローカル変数localVar
も、親と子で異なるアドレスが表示される。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
Goの軽量スレッドであるgoroutineを使用してメモリを観察する。goroutineは同一プロセス内で動作するため、仮想アドレス空間を共有する。 これを確かめるため、複数のgoroutineでグローバル変数とローカル変数のアドレスを表示してみる。
package main
import (
"fmt"
"os"
"runtime"
"sync"
"time"
)
var globalVar = 100
func worker(id int, wg *sync.WaitGroup) {
defer wg.Done()
localVar := id * 10
fmt.Printf("Goroutine %d: PID=%d, globalVar=%p (value=%d), localVar=%p (value=%d)\n",
id, os.Getpid(), &globalVar, globalVar, &localVar, localVar)
// グローバル変数を変更(意図的にdata raceを発生させる)
// 複数goroutineが同期なしに同じ変数へアクセス → 競合状態
globalVar += id
time.Sleep(100 * time.Millisecond)
}
func main() {
localVar := 5
fmt.Printf("Main: PID=%d, globalVar=%p (value=%d), localVar=%p (value=%d)\n",
os.Getpid(), &globalVar, globalVar, &localVar, localVar)
fmt.Printf("Number of goroutines: %d\n", runtime.NumGoroutine())
var wg sync.WaitGroup
for i := 1; i <= 3; i++ {
wg.Add(1)
go worker(i, &wg)
}
wg.Wait()
fmt.Printf("Final globalVar value: %d\n", globalVar)
fmt.Printf("Number of goroutines: %d\n", runtime.NumGoroutine())
}
Main: PID=19628, globalVar=0x1031503c8 (value=100), localVar=0x14000104020 (value=5)
Number of goroutines: 1
Goroutine 3: PID=19628, globalVar=0x1031503c8 (value=100), localVar=0x14000104050 (value=30)
Goroutine 1: PID=19628, globalVar=0x1031503c8 (value=100), localVar=0x14000180000 (value=10)
Goroutine 2: PID=19628, globalVar=0x1031503c8 (value=103), localVar=0x14000096000 (value=20)
Final globalVar value: 106
Number of goroutines: 1
globalVar
のアドレスが全goroutineで同じである。localVar
のアドレスを取得してfmt.Printf
に渡しているため、エスケープ解析によってヒープに配置される。それでも各goroutineで異なるメモリ領域に配置されており、独立性は保たれている。globalVar
の値が106になったのはたまたまで、実行タイミングによって結果は変わる。
安全に並行処理を行うには、チャネルやミューテックスなどの同期機構が必要である。
→ go run -race
で競合検出可能。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
チャネルを使えば、共有変数を直接更新せずにデータをやり取りできる。
package main
import (
"fmt"
"time"
)
var globalVar = 100
func worker(id int, ch chan<- string) {
localVar := id
ch <- fmt.Sprintf("Goroutine %d: globalVar=%p, localVar=%p", id, &globalVar, &localVar)
}
func main() {
ch := make(chan string)
for i := 0; i < 3; i++ {
go worker(i, ch)
}
for i := 0; i < 3; i++ {
fmt.Println(<-ch)
}
time.Sleep(100 * time.Millisecond)
}
Goでは動的メモリはヒープに置かれることが多いが、配置先はエスケープ解析で確認できる。
package main
import "fmt"
func main() {
heapSlice := make([]int, 3)
heapSlice[0] = 42
fmt.Printf("heapSlice addr: %p\n", &heapSlice[0])
}
heapSlice addr: 0x140000ac030
項目 | スタック | ヒープ |
---|---|---|
管理方法 | 関数呼び出しに伴い自動で確保・解放 | GCが管理 |
領域の独立性 | goroutineごとに独立 | プロセス全体で共有 |
速度 | 高速 | 相対的に遅い |
配置条件 | エスケープしない変数 | エスケープする変数 |