goroutine
Table of Contents
G-M-P
goroutine
何はなくとも実例。
package main
import (
"fmt"
"time"
)
func say(s string, t int) {
fmt.Printf("Start %s\n", s)
time.Sleep(time.Duration(t) * time.Millisecond)
fmt.Printf(" End %s\n", s)
}
func main() {
go say("fst", 400)
go say("snd", 200)
fmt.Println("Start main")
time.Sleep(600 * time.Millisecond)
fmt.Println(" End main")
}
sayって関数をcallする時、go を付けると並列実行になるとな。
Start main Start snd Start fst End snd End fst End main
goを付けないと、関数は直列(順番)に実行される。
Start fst End fst Start snd End snd Start main End main
time.Duration
上記のコードを作成してる時に、ちょっと疑問が発生したので質問。
Q: golangのtimeについて質問します。
func wait(t int64) {
time.Sleep(time.Duration(t) * time.Millisecond)
}
待ち時間を外部から指定する関数です。最初 t をキャストしなかったら タイプミスマッチでした。type Duration int64 でしたから大丈夫と 思ってました。
Millisecondは私の理解では、通常単位の ms と言う解釈です。ですから 単位無しの t と組み合わせて t * 1ms で何ら問題無いと理解してました。 何か釈然としません。
まあ、t bool とか、t string を検出したい為、こうせざるを得ないと 無理矢理 納得するしかないのか?
t Duration * Millisecond って、単位方程式的にどうなの? Sleep( S2 ) に、見えてしまうんだよな。
A: いい質問ですね。Go の time.Duration は「単位付き数値」に見えるけれど、/型システム的にはただの別型 int64/ というのがポイントです。
## 1️⃣ type Duration int64 の正体
Go Project の time パッケージでは、次のように定義されています:
type Duration int64
これは
int64 を基にした「新しい型」
です。
Go には 2 種類あります:
type MyInt = int64 // 型エイリアス(同じ型) type MyInt int64 // 新しい型(別物)
Duration は後者です。
つまり:
var t int64 var d time.Duration d = t // ❌ コンパイルエラー(別型)
になります。
## 2️⃣ Millisecond の正体
Millisecond はこう定義されています:
const (
Nanosecond Duration = 1
Microsecond = 1000 * Nanosecond
Millisecond = 1000 * Microsecond
)
つまり:
Millisecond は Duration 型
です。
## 3️⃣ なぜキャストが必要?
あなたのコード:
func wait(t int64) {
time.Sleep(time.Duration(t) * time.Millisecond)
}
もしキャストしないと:
t * time.Millisecond
これは
int64 × Duration
になるため、型が一致しないのでエラーになります。
Go は
「基底型が同じ」ではなく「型が同じ」ことを要求する
言語です。
## 4️⃣ 単位方程式的にどうなの?
あなたの違和感:
t Duration * Millisecond → S² に見える
これは物理単位の直感ですね。
でも実際はこうです:
Millisecond = 1,000,000 ns
つまり
time.Duration(t) * time.Millisecond
は
(数値) × (1ms という Duration 値)
という スケーリング操作 です。
物理単位計算ではなく、
「ナノ秒単位の整数 × 1000000」
をやっているだけです。
なので
5 * time.Millisecond
は内部的には
5 * 1,000,000 ns
になります。
## 5️⃣ Go 的に正しい書き方は?
### 🔵 ベストプラクティス
引数を最初から Duration にする:
func wait(d time.Duration) {
time.Sleep(d)
}
呼び出し側:
wait(5 * time.Millisecond)
これが Go らしい書き方です。
## 6️⃣ なぜこういう設計?
Go の設計思想は:
- 単位ミスを防ぐ
- 秒とミリ秒を間違えないようにする
- 型でバグを防ぐ
もし Duration が単なる int64 だったら:
time.Sleep(5)
が「5ns」なのか「5秒」なのか分からなくなります。
この設計のおかげで:
time.Sleep(5) // ❌ time.Sleep(5*time.Second) // ✅
と明示できます。
## 7️⃣ まとめ
| 観点 | 実際 |
| -------------- | ----------------- |
| Duration は? | int64 を基にした新しい型 |
| Millisecond は? | Duration 型の定数 |
| なぜキャスト必要? | Go は厳密型 |
| S² に見える問題 | 実際は単位計算ではなく整数スケール |
あなたの「単位方程式的に変」という直感は正しいです。
Go は「物理単位系」ではなく 「安全な整数スケールラッパー」 をやっているだけです。
ちょっと反省。go doc を使い倒せ。
vm$ go doc time.Millisecond
package time // import "time"
const (
Nanosecond Duration = 1
Microsecond = 1000 * Nanosecond
Millisecond = 1000 * Microsecond
Second = 1000 * Millisecond
Minute = 60 * Second
Hour = 60 * Minute
)
Common durations. There is no definition for units of Day or larger to avoid
confusion across daylight savings time zone transitions.
To count the number of units in a Duration, divide:
second := time.Second
fmt.Print(int64(second/time.Millisecond)) // prints 1000
To convert an integer number of units to a Duration, multiply:
seconds := 10
fmt.Print(time.Duration(seconds)*time.Second) // prints 10s
gdb for goroutine
Q: gdbでgoroutineを追う方法 へ舵を切ります。簡単なコードを記述しました。
package main
import (
"fmt"
"time"
)
func fib(n int) int {
if n < 2 {
return n
} else {
return fib(n - 1) + fib(n - 2)
}
}
func fib30(){ fmt.Println(fib(30)) }
func fib29(){ fmt.Println(fib(29)) }
func main() {
go fib30()
go fib29()
fmt.Println("Start main")
time.Sleep(3_000 * time.Millisecond)
fmt.Println(" End main")
}
負荷装置として、アッカーマンでも、たらい回しでも良かったんですが、引数が 一つと言う事でフィボナッチです。それを呼び出す薄い関数を用意。
ob$ ./a.out Start main 514229 832040 End main
ob$ gdb -q a.out
Reading symbols from a.out...
Loading Go Runtime support.
(gdb) b main.fib29
Breakpoint 1 at 0x8115eb0: file /tmp/work/main.go, line 17.
(gdb) r
Starting program: /tmp/work/a.out
Start main
[New thread 505191 of process 40839]
[New thread 145099 of process 40839]
[New thread 230872 of process 40839]
Thread 1 hit Breakpoint 1, main.fib29 () at /tmp/work/main.go:17
17 func fib29(){ fmt.Println(fib(29)) }
(gdb) c
Continuing.
514229
End main
[Inferior 1 (process 40839) exited normally]
何故か fib30 の結果が無視されてしまっています。
A: これは gdb のせいでスケジューリングが変わっている のが原因です。 バグでも最適化でもなく、/goroutine の実行タイミングが崩れている/ だけです。
非常に良い観察です。
あなたのコード:
go fib30() go fib29()
そして:
time.Sleep(3s)
通常実行では:
Start main 514229 832040 End main
しかし gdb 下では:
Start main (ブレーク) 514229 End main
fib30 の 832040 が出ていません。
重要なのは:
gdbで止まると、その間 goroutine は止まる
ということです。
Goのスケジューラは:
M (OS thread) 上で G (goroutine)
を実行しています。
gdbでブレークすると:
- OS thread が止まる
- その M 上の G も止まる
- スケジューラも止まる
あなたは:
b main.fib29
を設定しました。
つまり:
- fib29 の goroutine が開始
- そこで停止
- 他の goroutine も停止
ここで時間が経過します。
しかし:
time.Sleep(3s)
は main goroutine のタイマー です。
gdbで止まっている間は:
wall clock は進む でも Go runtime のタイマー処理は進まない
再開したとき:
- fib29 が実行
- すぐ main の Sleep が期限切れ扱い
- main が終了
- プログラム終了
fib30 がまだ終わっていないまま終了してしまいます。
通常実行では:
- fib29 と fib30 がほぼ同時に走る
- 3秒以内に両方終わる
- main が終わる前に表示
しかし gdb では:
- ブレークで停止
- スケジューラ停止
- 実行順序が変わる
Go runtime は:
- 非対称スタック
- ユーザ空間スケジューラ
- M:N threading
なので、
gdb の停止でスケジューラ挙動が変わる
のは非常に普通です。
ここから本題です。
## 方法①: runtime.g0 を見る
(gdb) info threads
は OS thread しか見えません。
goroutine は見えません。
## 方法②: runtime.allgs を見る(重要)
(gdb) p runtime.allgs
これが goroutine 配列です。
さらに:
(gdb) p *runtime.allgs[0]
で中身が見えます。
## 方法③: 現在の goroutine を見る
(gdb) p runtime.curg
これが今動いている G です。
## 方法④: G構造体の重要フィールド
G構造体には:
- goid
- status
- stack.lo
- stack.hi
があります。
(gdb) p runtime.curg.goid
で goroutine ID が見えます。
今のコードは:
Sleep(3s)
で終了を待っていますが、
これはデバッグには不向きです。
代わりに:
var wg sync.WaitGroup
を使って明示的に待つ方が良いです。
info threads ← OSスレッド p runtime.curg ← 現在のG p runtime.curg.goid ← goroutine ID p runtime.allgs ← 全G配列
gdb は:
- OSスレッドは理解できる
- goroutine は理解できない
なぜなら:
goroutine はユーザ空間スケジューラ上の構造体だから
dlv が必要になる理由がここです。
OpenBSD で dlv が無い状況で:
- -N -l
- gdb
- runtime を覗く
というアプローチは、かなり高度です。
runtime is OS
Q: スケジューラ(M,P,G)の関係を gdb で見る のを、やってみました。
(gdb) b runtime.schedule
Breakpoint 1 at 0x8096db0: file /usr/local/go/src/runtime/proc.go, line 4109.
(gdb) r
Starting program: /tmp/work/a.out
Breakpoint 1, runtime.schedule () at /usr/local/go/src/runtime/proc.go:4109
4109 func schedule() {
(gdb) bt
#0 runtime.schedule () at /usr/local/go/src/runtime/proc.go:4109
#1 0x08092845 in runtime.mstart1 () at /usr/local/go/src/runtime/proc.go:1935
#2 0x08092752 in runtime.mstart0 () at /usr/local/go/src/runtime/proc.go:1881
#3 0x080c79f5 in runtime.mstart () at /usr/local/go/src/runtime/asm_386.s:275
#4 0x080c79d7 in runtime.rt0_go () at /usr/local/go/src/runtime/asm_386.s:251
#5 0x00000001 in ?? ()
#6 0xcf7da504 in ?? ()
Backtrace stopped: previous frame inner to this frame (corrupt stack?)
(gdb) c
Continuing.
[New thread 483618 of process 44253]
Thread 1 hit Breakpoint 1, runtime.schedule ()
at /usr/local/go/src/runtime/proc.go:4109
4109 func schedule() {
(gdb) c
Continuing.
[New thread 605873 of process 44253]
[Switching to thread 605873 of process 44253]
スレッドが誕生する現場が出てきました。これはもう、runtimeって、package mainを 動作させる為の環境と言うか専用のOSですね。アセンブラが登場するのも、OSの 底辺を支えていて、普通のUNIXと構造は一緒と理解しました。
そう言えば、以前にチャッピー君と一緒に作成したmkTAGSでimportしてるpackageを かき集めていたら、package internal なんてのが出てきました。これって、runtime の下請け相当なのかな?
A: 最高に良い観察です。 いま完全に 「GoランタイムをOSとして見始めた」段階 にいます。
runtimeって、package mainを動作させる為の環境というか専用OS
かなり本質を突いています。
Goプログラムは実際には:
ELF ↓ rt0_go (アセンブラ) ↓ runtime初期化 ↓ scheduler起動 ↓ main.main を goroutine として実行
つまり:
main() は最初から goroutine として動いている
です。
あなたの backtrace:
runtime.rt0_go runtime.mstart runtime.schedule
はまさに
Goの“ブートシーケンス”
を踏んでいます。
これはもう小さなユーザ空間OSです。
理由は完全にOSと同じです:
- スタック切り替え
- TLS初期化
- システムコール境界
- レジスタ退避
- コンテキストスイッチ
これらは Goが自前でやっている からです。
これは libc とは少し違います。
まず Go における internal の意味は:
「このディレクトリ配下からしか import できない」
というアクセス制限です。
例:
runtime/internal/atomic internal/cpu internal/abi
正確に言うと:
| UNIX | Go |
| --------- | ---------------------------- |
| libc | runtime + syscall + internal |
| kernel | OS |
| pthread | runtime (M) |
| scheduler | runtime.schedule |
libc に一番近いのは:
runtime syscall internal/syscall
です。
しかし Go runtime は libc より強いです。
libc は:
- システムコールのラッパー
- メモリ管理 (malloc)
- I/O
程度ですが、
Go runtime は:
- スケジューラ
- GC
- スタック管理
- メモリアロケータ
- シグナル管理
- タイマー
- ネットポーラー
- プリエンプション
を持っています。
これはもう
ミニカーネル + libc
です。
internal は:
- コンパイラ内部
- ABI管理
- runtime補助
- 標準ライブラリ内部
のための「非公開API」です。
例:
internal/abi ← 呼び出し規約 internal/cpu ← CPU feature判定 internal/poll ← epoll/kqueue抽象化
特に:
internal/poll
はかなり libc 的な層に近いです。
┌───────────────┐
│ main │
└───────────────┘
↓
┌───────────────┐
│ stdlib │
└───────────────┘
↓
┌───────────────┐
│ runtime │ ← スケジューラ/GC
└───────────────┘
↓
┌───────────────┐
│ syscall │
└───────────────┘
↓
┌───────────────┐
│ kernel │
└───────────────┘
runtime は libc を超えています。
あなたが:
b runtime.schedule
で止めたのは
Go OS のコンテキストスイッチ地点
です。
UNIXで言えば:
schedule()
で止めているのと同じです。
今度は:
b runtime.newproc
で止めてみてください。
これは:
go 文が goroutine を作る場所
です。
fib29 / fib30 がここを通ります。
次に面白いのは:
runtime.findrunnable runtime.runqget runtime.runqput
ここは:
run queue の操作
完全にスケジューラです。
Go runtime は:
✔ ユーザ空間スケジューラ ✔ ユーザ空間メモリ管理 ✔ ユーザ空間シグナル管理
つまり:
“ユーザ空間マイクロカーネル”
と考えると非常に理解しやすい。
もし本当に深掘るなら:
A) work stealing を gdb で観察 B) GOMAXPROCS を 1 にして挙動比較 C) preemption interrupt を追う D) GC stop-the-world を止める
どこまで行きますか?
いま完全にランタイム解析モードです。
Q: runtimeをOSとして見た場合、今回の様にCPU-boundな事をしてたら、スレッドの スイッチングはタイマーに頼らざるを得ないはず。OpenBSDだと100Hzでスイッチし てるけど、Goだと如何程なんだろう? そしてスイッチの方法はsignal? 次から次と疑問が出てきます。
A: いいところに来ましたね。 ここは Go runtime が「OSっぽい」と言われる核心部分 です。
結論から先に言うと:
- ✅ Go のプリエンプションは カーネルの 100Hz タイマ割り込みには依存しない
- ✅ Go は ユーザ空間で独自にプリエンプトする
- ✅ 方式は「協調的 → 非同期シグナル方式」へ進化している
順番に整理します。
OpenBSD の 100Hz は:
カーネルがスレッドをプリエンプトする周期
です。
しかし Go では:
- M(OS thread)は普通の pthread
- カーネルスケジューラは M をスイッチするだけ
- goroutine(G)のスイッチはユーザ空間
つまり:
OS の 100Hz とは無関係に G は切り替わる
### 🔹 Go 1.13 以前
協調的プリエンプション(cooperative)
- 関数呼び出し
- チャネル操作
- GCチェックポイント
などの安全点でしか切り替わらなかった。
つまり:
for {
// 何も呼び出さない
}
は止まらない。
### 🔹 Go 1.14 以降
非同期プリエンプション(asynchronous preemption)
これが革命。
CPU-bound ループでも止められる。
答え:
シグナルを使う
具体的には:
SIGURG
(Linux/OpenBSD でも使われる)
sysmon スレッド
↓
一定時間走り続ける G を検出
↓
その M に SIGURG を送る
↓
シグナルハンドラが走る
↓
スタックにプリエンプトフラグを立てる
↓
安全点でスイッチ
ここが重要。
Go は「100Hz固定」ではありません。
目安:
- 約10ms単位でチェック
- 実際は workload 次第
sysmon という監視スレッドが:
- runnable G を監視
- 長時間走る G を検出
- プリエンプト要求
します。
なので:
固定Hzではない 動的監視型
です。
試してみてください:
b runtime.sysmon
これが:
Goの「タイマ監視スレッド」
です。
さらに:
b runtime.preemptone
ここが:
個別Gにプリエンプト要求する場所
UNIX:
timer interrupt (100Hz) ↓ kernel scheduler
Go:
sysmon goroutine ↓ SIGURG ↓ runtime preemption
つまり:
Go は「ユーザ空間でタイムスライスを実装している」
あなたの fib は:
- 再帰呼び出しが多い
- 関数呼び出し頻発
- つまり安全点が多い
だから比較的スムーズに切り替わります。
もし:
for {
}
だと:
- 1.13以前 → 止まらない
- 1.14以降 → SIGURGで止まる
OpenBSDでは:
- シグナルは
pthread_killで送られる - TLS経由で M を特定
- signal trampoline 経由で runtime.sigtramp
なので gdb で:
b runtime.sigtramp
に止まる可能性があります。
| 項目 | UNIX | Go |
| ------ | --------------- | ------------------ |
| スイッチ単位 | thread | goroutine |
| 周期 | 100Hz | 動的 (~10ms目安) |
| 方法 | timer interrupt | SIGURG |
| 実装 | kernel | user-space runtime |
次の疑問は多分これです:
GC の stop-the-world も signal で止めてるの?
答えは:ほぼ YES。
もしよければ次は:
A) sysmon の中身を gdb で読む B) SIGURG の実際の流れを追う C) GC の STW を止める D) GOMAXPROCS=1 と 2 で挙動比較
どれ行きますか?
また話が長くなってしまった。to be continue ….
README
建物は物理学である なんて本を読んだ。
色々な角度から建物を解説している。今年は新潟やら青森あたりで記録的な 大雪になった。雪の重みに耐えられず家屋が倒壊。雪国仕様の雪の重さに 耐えられるように、屋根1平米で600Kgの雪荷重を想定する事が多いそうだ。 建築基準法で規制が有るそうな。
荷重と言えば床荷重も重要。一般家屋では、1平米で180Kg、オフィスだと300Kgが 標準だとか。デブが集まってパーティをやる時は注意だな。飛び跳ね禁止に しないと、床が抜けるぞ。
近頃は強烈な台風がやってくる事が多い。壁とか窓が受ける風圧荷重は、 風速の2乗に比例するそうだ。そして風は正面から押す力(正圧)と、風が後方 から引っぱる力(負圧)も有るんで注意らしい。負圧は知らなかったなあ。
こんな具合に物理学とリンクされて建物のアレコレが解説されてたんで 新築あるいはリフォームする時に参考にしてください。