go thread

Table of Contents

pthread

前回まで goroutineを調べてきた(G-M-P)。が、その根底にあるOSのスレッドの事を 知らないので、調査してみる。

私の理解では、プロセスって、ulimit -a に表示されるリソース群の管理単位。 fork -> execve で、アプリがメモリーにロードされ、mainから実行を 開始する。CPUが複数有ったら、そこで重い計算処理とかを手分けしてやりたい。 それを可能にするのがスレッドだ。

アプリがメモリーにロードされているんで、そのメモリーの特定部分(関数)の 実行をそれぞれのCPUに担当させてしまえば良い。

その為のpthreadと言うライブラリィーが提供されてる。

20秒間スレッドが稼動。その後10秒間だけメインが居残るやつ。

#include <pthread.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>

void *th_func(void *arg) {
    puts("thread created");
    sleep(20);
    return NULL;
}

int main(int argc, char *argv[])
{
    pthread_t th;

    if (pthread_create(&th, NULL, th_func, NULL) == 0) {
        puts("Main thread");
        sleep(30);
    }
}

これ、go的には、

main(){
  go th_func()
  time.Sleep(30 * time.Second)
}

コンパイルして実行。勿論OpenBSD上で動かす。だって、深い所まで容易に 潜って行けるから。まあ、Ubuntuでどうやればいいか、知らないだけだけど。

qe$ cc -g p.c -lpthread
qe$ ./a.out &
[1] 46724
qe$ Main thread
thread created
ps -aH ;; -HでスレッドIDも表示(リナだと -Lらしい)
  PID     TID TT  STAT        TIME COMMAND
46724  491566 p1  S        0:00.08 ./a.out
46724  250749 p1  S        0:00.00 ./a.out
59565  178705 p1  R+pU     0:00.03 ps -aH
qe$ ps -aH ;; 20秒経過するとメイン・スレッドだけになる。
  PID     TID TT  STAT        TIME COMMAND
46724  491566 p1  I        0:00.08 ./a.out
 3835  266640 p1  R+pU     0:00.02 ps -aH

詳細な使用方法は、PTHREADS(3)

下記はユーザーランド側

(gdb) bt
#0  pthread_create (threadp=.., attr=.., start_routine=.., arg=..)
 at /usr/src/lib/librthread/rthread.c:346
#1  0x00000516e5e2db0e in main (argc=1, argv=0x7576305e9a58) at p.c:16

そして、syscallする瞬間と渡したい引数(gdb調べ)

          tib = _dl_allocate_tib(sizeof(*thread));
          thread = tib->tib_thread;
          thread->stack = _rthread_alloc_stack(thread);
           :	  
          /* we're going to be multi-threaded real soon now */
          __isthreaded = 1;
  =>      rc = __tfork_thread(&param, sizeof(param), _rthread_start, thread);

(gdb) p param
$4 = {
  tf_tcb = 0x5191d421a00,
  tf_tid = 0x5191d421a30,
  tf_stack = 0x51993318800
}
(gdb) p _rthread_start
$5 = {void (void *)} 0x51976b01f10 <_rthread_start>

こちらはカーネル側

(gdb) bt 5
#0  cpu_fork (p1=.., p2=.., stack=.., tcb=.., func=.., arg=..)
    at /usr/src/sys/arch/amd64/amd64/vm_machdep.c:67
#1  0xffffffff820f2bf6 in thread_fork (curp=.., stack=0x256f93950, tcb=0x2a8796a08, tidptr=.., retval=..)
    at /usr/src/sys/kern/kern_fork.c:579
#2  0xffffffff820f2a1c in sys___tfork (p=0x256f93950, v=<optimized out>, retval=0x2a8796a08)
    at /usr/src/sys/kern/kern_fork.c:138
#3  0xffffffff81ee33d8 in mi_syscall (p=.., code=-140736891650752, argp=.., retval=.., callp=..)
    at /usr/src/sys/sys/syscall_mi.h:179
#4  syscall (frame=0xffff80002390fcc0)
    at /usr/src/sys/arch/amd64/amd64/trap.c:577
(More stack frames follow...)

qemu+kvmな環境なんだけど、カーネルがシングルプロセッサー用に コンパイルされ(して)たので、MPには、なっていない。

ad$ sysctl | grep cpu
kern.ccpu=1948
hw.ncpu=1
hw.cpuspeed=2401
hw.ncpufound=4
hw.ncpuonline=1
machdep.cpuvendor=GenuineIntel
machdep.cpuid=0x60fb1
machdep.cpufeature=0x179bfbfd

下記はamd64なリアルマシン(OpenBSD/河豚板版)

fu$ sysctl | grep cpu
kern.ccpu=1948
hw.ncpu=4
hw.ncpufound=4
hw.ncpuonline=2
machdep.cpuvendor=GenuineIntel
machdep.cpuid=0x406e3
machdep.cpufeature=0xbffbfbff

昔はマスター/スレーブって呼称だったけど、今は無難な名称になってる。

fu$ dmesg | grep processor
cpu0 at mainbus0: apid 0 (boot processor)
cpu1 at mainbus0: apid 2 (application processor)
cpu2 at mainbus0: apid 1 (application processor)
cpu3 at mainbus0: apid 3 (application processor)

OpenBSDは控え目なんで、topで確認するとcpu0,1 が利用可能になってる。

MULTIPROCESSOR

本題はここからで、ソースをちょっと拝見。実体は9000行近くある。 on /sys/arch/amd64/amd64/vmm_machdep.c

/*
 * vmm_start
 *
 * Starts VMM mode on the system
 */
int
vmm_start(void)
{
        int rv = 0;
        struct cpu_info *self = curcpu();
#ifdef MULTIPROCESSOR
        struct cpu_info *ci;
        CPU_INFO_ITERATOR cii;
#ifdef MP_LOCKDEBUG
        long nticks;
#endif /* MP_LOCKDEBUG */
#endif /* MULTIPROCESSOR */
  :
#ifdef MULTIPROCESSOR
        /* Broadcast start VMM IPI */
        x86_broadcast_ipi(X86_IPI_START_VMM);

        CPU_INFO_FOREACH(cii, ci) {

ちょっと興味が有りそうな関数を探してみた。

acpi_machdep.c:510:     x86_broadcast_ipi(X86_IPI_HALT);
amd64_mem.c:281:        x86_broadcast_ipi(X86_IPI_MTRR);
cpu.c:1473:     x86_broadcast_ipi(X86_IPI_WBINVD);
ipi.c:69:x86_broadcast_ipi(int ipimask)
machdep.c:933:  x86_broadcast_ipi(X86_IPI_HALT);
mp_setperf.c:42:        x86_broadcast_ipi(X86_IPI_SETPERF);
pctr.c:164:             x86_broadcast_ipi(X86_IPI_PCTR);
vmm_machdep.c:665:      x86_broadcast_ipi(X86_IPI_START_VMM);
vmm_machdep.c:725:      x86_broadcast_ipi(X86_IPI_STOP_VMM);

cpu0で動作してるプロセスが、どうやってcpu1でスレッドを動かすか? 軽く調べた所、CPU間でIPI (Inter-Processor Interrupt)と言う、割り込みを してるみたい。そして要はICR (Interrupt Command Register)って制御用の レジスタらしい。

SEE: https://qiita.com/Egh2Deywos/items/f9c04f5d308f64bdcac5

Q1: 多分、この機能を使って、cpu1側でスレッドを起動すると思うんだけど、
    具体的には、どうやっているんだろう?
    SYS___tfork を見据えて、解説を希望。
Q2: それから今更なんだけど、pthreadを使用しない普通のアプリでも、ps -aH すると
    TIDが付与されてた。カーネル内でもtforkをこっそり利用してるの?

A: かなり深い所まで追っていますね。OpenBSDで gdb を使って pthread_create → __tfork_thread → sys___tfork まで辿っているので、ほぼ「ユーザランド→カーネル入口」は完全に理解されています。 質問の核心は 「その後どうやって別CPUで走るのか」「pthreadを使わないのにTIDがある理由」 の2点ですね。順に説明します。


(=sys__tfork= を起点)

まず大きな流れを整理します。

pthread_create
   ↓
__tfork_thread (libpthread / librthread)
   ↓ syscall
sys___tfork
   ↓
thread_fork
   ↓
cpu_fork
   ↓
run queue に追加
   ↓
scheduler がCPUに割り当て
   ↓
必要なら IPI で他CPUを起こす

ポイントは tfork自体はCPUを指定して起動しない ということです。 /スレッドを作るだけで、どのCPUで走るかは scheduler が決める/。


ユーザランドから渡された構造体

param = {
  tf_tcb
  tf_tid
  tf_stack
}

これをカーネルに渡します。

sys___tfork(...)

ここでやっていることは

  • 新しい struct proc
  • 新しい struct thread (OpenBSDではprocに近い)
  • stack
  • TLS (tcb)

の設定です。

そのあと呼ばれるのが

thread_fork()

ここが /カーネルスレッド生成の本体/です。

概念的には

thread_fork(curp, stack, tcb, tidptr, retval)
{
    p2 = newproc()

    cpu_fork(curp, p2, stack, tcb, func, arg)

    set runnable

    scheduler queue に入れる
}

ここで重要なのは

setrunnable(p2)

これにより /run queueに追加/されます。


ここはアーキ依存 (vm_machdep.c) で

あなたが見ている場所です。

主な仕事は

  • trapframeコピー
  • registerコピー
  • kernel stack設定

つまり

新しいCPUコンテキストを作る

ここで

func = _rthread_start
arg  = thread

が設定されます。

つまり新しいスレッドは

_rthread_start(thread)

から実行されます。


ここまででスレッドは

state = runnable

になります。

すると scheduler が

次に実行するスレッド

として選びます。


もし

CPU0 で thread_fork

が呼ばれたとしても

schedulerは

CPU1のrun queue

に入れることがあります。

もし CPU1 が idle なら

IPI を送る

あなたが見つけた

x86_broadcast_ipi()

これは典型的なSMP動作です。

CPU0が

APIC ICR register

を書きます。

すると

CPU1 に interrupt

が飛びます。

CPU1は

interrupt handler
    ↓
scheduler
    ↓
run queue
    ↓
new thread start

という流れ。

つまり

IPIはスレッドを起動するためではなく

他CPUに scheduler を走らせる

ために使われます。


CPU1でスレッドが走る流れ

pthread_create
   ↓
sys___tfork
   ↓
thread_fork
   ↓
cpu_fork
   ↓
setrunnable
   ↓
scheduler
   ↓
run queue
   ↓
(必要なら) IPI
   ↓
CPU1がスケジューリング
   ↓
_rthread_start

つまり

tfork → scheduler → IPI

です。


これは非常に良い観察です。

答えは

OpenBSDではプロセス自体がスレッドとして扱われるからです。

内部構造は

struct proc

で統一されています。

Linux的に言うと

task_struct

に近い。


## つまり

pthreadなしの普通のプログラムでも

main thread

が1個存在します。

だから

PID = process id
TID = thread id

になります。

PID   TID
1000  1000

最初のスレッドは

TID == PID

です。


## pthreadcreateすると

PID   TID
1000  1000   main thread
1000  1001   worker

になります。


これは

NO

です。

正確には

fork()

fork1()

を呼びますが

内部的には

thread_fork()

を共有しています。

つまり

fork
tfork
kthread

すべて

thread_fork()

に集約されています。


あなたの調査はかなり核心に近いです。

理解のポイントはこれです。

スレッド生成とCPU割り当ては別問題

thread_fork
   = スレッド作る

scheduler
   = CPUに割り当てる

IPI
   = 他CPUを起こす

with go

Q: 非力なi38マシンで、下記の様な負荷を2個、goroutimeしてみる。

func cale(n int) {
        rv := 0.0
        for i := 1; i < n*1_000_000; i++ {
                rv = math.Log(math.Sqrt(float64(i)))
        }
        fmt.Println(rv)
}
Breakpoint 1, pthread_create (threadp=0xcf7e4914, attr=0xcf7e49b0,
    start_routine=0x80c99b0 <runtime.mstart_stub>, arg=0x52836008)
    at /usr/src/lib/librthread/rthread.c:356
356             if (!_threads_ready)
(gdb) c
Continuing.
[New thread 145920 of process 97557]

Thread 1 hit Breakpoint 1, pthread_create (threadp=0xcf7e4884,
    attr=0xcf7e4920, start_routine=0x80c99b0 <runtime.mstart_stub>,
    arg=0x52836808) at /usr/src/lib/librthread/rthread.c:356
356             if (!_threads_ready)
(gdb) bt
#0  pthread_create (threadp=0xcf7e4884, attr=0xcf7e4920,
    start_routine=0x80c99b0 <runtime.mstart_stub>, arg=0x52836808)
    at /usr/src/lib/librthread/rthread.c:356
#1  0x080c9b1a in runtime.pthread_create_trampoline ()
    at /usr/local/go/src/runtime/sys_openbsd_386.s:167
#2  0xcf7e4920 in ?? ()
#3  0x080c8d3c in runtime.asmcgocall ()
    at /usr/local/go/src/runtime/asm_386.s:689
#4  0xcf7e48e4 in ?? ()

ここで起動したthreadは、<runtime.mstartstub>, arg=0x52836808 だけど、 実体は何だろう? attrで、どんな性格を与えているの?

A: いい所にブレークポイントを置いています。 その pthread_create → runtime.mstart_stub は /Go runtime が OS スレッド (M) を生成する瞬間/です。 順に整理します。


あなたの gdb の結果はこうです。

pthread_create(
    start_routine = runtime.mstart_stub
    arg           = 0x52836808
)

これは

新しい OS thread (M) を作る
        ↓
thread entry = runtime.mstart_stub
        ↓
mstart
        ↓
Go scheduler

つまり

pthread_create
   ↓
runtime.mstart_stub
   ↓
runtime.mstart
   ↓
schedule()
   ↓
goroutine 実行

になります。


あなたの backtrace にある

runtime/sys_openbsd_386.s

ここに定義されています。

実体は /assembly の小さなスタブ/です。

概念的にはこんなコードです。

TEXT runtime·mstart_stub(SB),NOSPLIT,$0
    CALL runtime·mstart(SB)

役割はこれだけです。

理由は

pthread は C ABI で呼ばれる

void *start_routine(void *)

だからです。

しかし Go runtime は

g
m
stack
TLS

を初期化してから mstart() に入りたい。

だから

pthread
  ↓
mstart_stub (ABI調整)
  ↓
mstart

という構造になっています。


これは /Go runtime の M 構造体/です。

Go runtime のコア構造体は3つ

G = goroutine
M = machine (OS thread)
P = processor

pthread_create では

arg = M*

が渡されています。

つまり

pthread thread
   ↓
mstart_stub(arg=M*)
   ↓
このthreadのMを設定

になります。


runtime/proc.go

ここが Go scheduler の入口です。

概念コードはこんな感じ

func mstart() {
    mstart1()
}

func mstart1() {
    acquirep()
    schedule()
}

つまり

OS thread 起動
   ↓
P を取得
   ↓
goroutine を実行

になります。


gdbでは

attr=0xcf7e4920

となっていますね。

Go runtime は pthread attribute を設定しています。

場所

runtime/os_openbsd.go

概念コード

pthread_attr_init(&attr);
pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED);
pthread_attr_setstacksize(&attr, stacksize);

主に設定されるのはこれです。

### (1) Detached thread

PTHREAD_CREATE_DETACHED

つまり

pthread_join 不要

Go runtime が自分で管理するからです。


### (2) stack size

Goは

OS thread stack

を固定サイズで作ります。

典型値

2MB

goroutine の stack は

2KB (start)

なので別物です。


あなたのログ

[New thread 145920]

その後

pthread_create again

理由は

main thread
+
scheduler worker thread
+
sysmon thread

Go runtime は起動時にいくつかスレッドを作ります。

代表的なのは

sysmon

です。

これは

GC
preemption
timer
network poll

などを監視します。


あなたの負荷

go calc()
go calc()

でも

pthread_create は2回とは限らない

理由

goroutine → M に multiplex

です。

つまり

2 goroutine
1 OS thread

でも実行可能です。

CPUが1個なら特にそうです。


sysctl の結果

hw.ncpu=1

つまり

GOMAXPROCS = 1

になる可能性が高いです。

すると

goroutine 2個
↓
OS thread 1個
↓
time slice

になります。


あなたが観測した

pthread_create
  start = runtime.mstart_stub

これはつまり

Go runtime が M (OS thread) を生成する瞬間

です。

流れはこうなります。

go calc()
     ↓
new goroutine (G)
     ↓
scheduler queue
     ↓
必要なら new M
     ↓
pthread_create
     ↓
mstart
     ↓
goroutine 実行

あなたの今の調査レベルなら 次のブレークポイントはここです。

runtime.newm

ここが

Go runtime が pthread_create を決断する場所

です。

ここを見ると

G
↓
P
↓
M
↓
pthread

の関係が /完全に理解できます/。

希望があれば、次は

「runtime.newm → pthread_create に到達するまで」

をソース付きで解剖します。 ここは Go runtime の一番面白い部分です。

ロードバランス

Q: ご指摘の様にシングルプロセッサーです。gdb上で走らせてスレッドの負荷分散を 確認。

ob$ ps -aH
  PID     TID TT  STAT        TIME COMMAND
63655  136072 p2  I        0:00.11 gdb a.out (egdb)
67177  322812 p2  RX+      1:15.89 /tmp/work/a.out
67177  422637 p2  IX+      0:00.00 /tmp/work/a.out
67177  407719 p2  IX+      0:00.00 /tmp/work/a.out
67177  588761 p2  SX+      0:00.06 /tmp/work/a.out

実質1つのスレッドしか動いていないなあ。

(gdb) info threads
  Id   Target Id                      Frame
  1    thread 322812 of process 67177 0x080a664a in runtime.fpack64 (sign=0,
    mant=26215522285846953, exp=-5, trunc=1376, ~r0=<optimized out>)
    at /usr/local/go/src/runtime/softfloat64.go:107
 * 2    thread 422637 of process 67177 _thread_sys___thrsleep () at /tmp/-:2
  3    thread 588761 of process 67177 _thread_sys_nanosleep () at /tmp/-:2
  4    thread 407719 of process 67177 _thread_sys___thrsleep () at /tmp/-:2

もう一つ、計算スレッドが存在すると思っていたぞ。シングルプロセッサーの 場合は、(負荷状況により)並行動作すら遠慮しちゃうの?

A: 観察はかなり正確です。結論から言うと、

Go はシングル CPU の場合、CPU-bound な goroutine を並行に走らせるための追加 OS スレッドを基本的に作りません。

なのであなたの観測

322812  RX+   ← 計算している
422637  IX+   ← sleep
407719  IX+   ← sleep
588761  SX+   ← sysmon

は /Go runtime の設計どおりの動作/です。

順番に説明します。


Go runtime の一番重要なパラメータは

GOMAXPROCS

です。

これは

同時に実行できる goroutine 数

ではなく

同時にCPUを使える OS thread 数

です。

内部構造は例の

G = goroutine
M = OS thread
P = processor token

実行条件は

G が走るには
M + P が必要

です。

つまり

M + P + G

の3つが揃って初めて実行できます。


Go 起動時に

GOMAXPROCS = hw.ncpu

になります。

あなたの環境

hw.ncpu=1

なので

P = 1

です。

つまり

同時実行できる goroutine = 1

です。


あなたのコード

go cal(100)
go cale(100)

内部状態はこうです

G1 calc
G2 cale

runqueue:

[G1 G2]

しかし

P = 1

なので

スケジューラは

G1 実行
↓
G2 待機

になります。


Go runtime は

CPU-bound workload

の場合

スレッドを増やしません

理由は単純です。

CPU が1個しかないのに

thread1
thread2

を作っても

context switch

が増えるだけだからです。

なので

M = 1
P = 1

で固定されます。


あなたの info threads

1 runtime.fpack64
2 __thrsleep
3 nanosleep
4 __thrsleep

これは典型的な Go プロセスです。

### thread1

計算スレッド

goroutine を実行


### thread2

sysmon

Go runtime の監視スレッド

役割

preemption
timer
GC
netpoll

### thread3

timer thread

time.Sleep


### thread4

runtime worker

idle worker


ここが一番重要です。

あなたの計算

for i := 1; i < n*1_000_000; i++ {
    rv = math.Log(math.Sqrt(float64(i)))
}

これは

CPU-bound
+
syscallなし
+
channelなし

です。

Go scheduler は

cooperative preemption

なので

goroutine が自発的に yield しないと切り替わりません。


最近の Go では

async preemption

があります。

sysmon が

SIGURG

goroutine interrupt

をかけます。

しかし

softfloat64

のような tight loop だと

/preemption point が少ない/です。

そのため

G1
↓
長時間独占

が起きます。


goroutine 切り替えを見たいなら

こうします

for i := 0; i < n*1_000_000; i++ {
    rv = math.Log(math.Sqrt(float64(i)))
    runtime.Gosched()
}

すると

G1
↓
yield
↓
G2
↓
yield

になります。


GOMAXPROCS=2

にします。

GOMAXPROCS=2 ./a.out

または

runtime.GOMAXPROCS(2)

すると

P = 2

になり

pthread_create

がもう一回発生します。


もし次に gdb を使うなら

ここにブレークポイント置くと Go runtime の理解が一気に進みます。

b runtime.schedule

ここで

G
P
M

の割り当てが全部見えます。

さらに面白いのは

b runtime.newm

です。

ここは

Go runtime が pthread_create を決断する瞬間

です。


もし興味あれば次は 「sysmon が goroutine を preempt する瞬間」 を gdb で追う方法を説明できます。

これ、Go runtime の中でも /一番 OS に近い部分/でかなり面白いです。

autogen

Q: お勧めの観察ポイントで止めてみました。

(gdb) bt
#0  runtime.newm (fn={void (void)} 0x0, pp=0x0, id=-1)
    at /usr/local/go/src/runtime/proc.go:2848
#1  0x00000000004724c7 in runtime.main.func1 ()
    at /usr/local/go/src/runtime/proc.go:174
#2  0x000000000047aa6e in runtime.systemstack ()
    at /usr/local/go/src/runtime/asm_amd64.s:513
#3  0x000000000047ebc5 in runtime.newproc (fn=0x0) at ./<autogenerated>:1
#4  0x000000000059ece0 in ?? ()

frame 3 で at ./<autogenerated>:1 と言ってきてるけど、コンパイル時に 自動作成されたもの? それとも frame 4 あたりが動的に作成したもの?

A: いい観察ポイントです。結論から言うと:

./<autogenerated>:1 は「コンパイル時に Go コンパイラが自動生成したスタブ関数」です。 frame 4 のような /動的生成コードではありません/。Go には JIT が無いので、実行時に新しいコードが生成されることは基本ありません。

順に整理します。


あなたの backtrace

#3 runtime.newproc (fn=0x0) at ./<autogenerated>:1

これは /Go コンパイラが作る goroutine 起動スタブ/です。

Go で

go f(x, y)

と書くと、コンパイラは内部でこう変換します(概念):

tmp := func() {
    f(x, y)
}
runtime.newproc(&tmp)

この /=tmp= 関数/が

<autogenerated>

です。

つまり

goroutine wrapper

です。


runtime.newproc のシグネチャはこうです(概念):

func newproc(fn *funcval)

つまり

引数なし関数ポインタ

を受け取ります。

しかしユーザコードは

go cal(100)

なので

引数あり

です。

そこでコンパイラが

func wrapper() {
    cal(100)
}

を自動生成します。


あなたの stack

runtime.main.func1
  ↓
runtime.systemstack
  ↓
runtime.newproc
  ↓
<autogenerated>

これは

runtime.main
  ↓
go cal()
  ↓
wrapper
  ↓
runtime.newproc

という流れです。


これも /コンパイラ生成関数/です。

runtime.main のソースを見ると

systemstack(func() {
    newproc(main_main)
})

というコードがあります。

この

func() { newproc(main_main) }

runtime.main.func1

です。


Go コンパイラはこう名前を付けます。

pkg.func1
pkg.func2
pkg.func3

これは

匿名関数
closure

です。

つまり

runtime.main.func1

runtime.main 内の匿名関数

です。


#4 0x59ece0 in ?? ()

これはたぶん

goroutine wrapper

の実体です。

なぜ ?? になるかというと

Go のデバッグ情報は

inline
wrapper

で崩れやすいからです。

次を試すと見えることがあります。

(gdb) info symbol 0x59ece0

または

(gdb) disas 0x59ece0

もし次をやると、goroutine の作成が完全に見えます。

b runtime.newproc

そのあと

p fn

すると

goroutine entry function

が見えます。


あなたの stack を整理すると:

runtime.main
   ↓
runtime.main.func1 (anonymous func)
   ↓
runtime.systemstack
   ↓
runtime.newproc
   ↓
goroutine wrapper (<autogenerated>)
   ↓
user function (cal)

です。


もし次に止めるならここです:

b runtime.newproc1

ここは /goroutine 構造体 G を作る場所/です。

ここで

p *gp

すると

stack
status
sched
goid

全部見えます。


さらに OS に近い部分を見るなら:

b runtime.schedule

ここで

p *m
p *p
p *g

を見ると

G-M-P scheduler

が /完全に理解できます/。


もし興味があれば次は 「Go の goroutine stack がどう作られるか (g0 / g / stack split)」 を説明できます。

これは

runtime.newproc
↓
runtime.malg
↓
runtime.stackalloc

という流れで、 /goroutine の 2KB stack が作られる瞬間/が見えるのでかなり面白いです。

また、長くなってしまった。何か木を見て森を見ずって雰囲気なんで、次回は 別の角度から攻めてみるか。

README

図書館から借りてきたセラミックって本を見ていたら、分子構造の 解説が出てた。詳しい解説は姉妹本って事だったけど、蔵書が 無かった。詳しい解説をAIに委ねる。

Q: 水の分子は、酸素を頂点に水素が104.5度で結合してるそうです。 またメタンだと、炭素を頂点に水素が109.5度で結合してるとか。

メタンだと CH4 だから、3角ピラミッドの頂点に水素を配置、ピラミッドの 重心に炭素が有るって構造なら、それぞれの水素が一番遠くに配置される 構造と思える。その結果、角度は幾何学的に決まる事になる。

その論を水に適用すると、水素は、酸素を中心に最も遠い位置、180度に なると想像できるんだけど、そうではないみたい。

どういう理論で決定されるの?

A: とても良い視点です。 結論から言うと、/分子の形は「電子対どうしができるだけ反発しない配置」をとる/ことで決まります。 これを説明する代表的な理論が VSEPR理論(Valence Shell Electron Pair Repulsion theory) です。


## ■ メタン(CH₄)の場合

メタン では、炭素のまわりに

  • 4つの共有電子対(C–H結合)
  • 孤立電子対なし

があります。

電子対は互いに反発するので、/できるだけ離れる配置/をとります。 4つの点が最も離れる立体配置は:

正四面体構造(tetrahedral)

このときの角度が 109.5°

これは幾何学的に決まります。


## ■ 水(H₂O)の場合

水 では、酸素のまわりに

  • 2つの共有電子対(O–H結合)
  • 2つの孤立電子対(非共有電子対)

があります。

つまり酸素の周りには 合計4つの電子対 があるのです。

ここがポイントです。


## ■ 水も「基本形」は正四面体

電子対が4つあるので、/電子対の配置自体は正四面体/になります。

しかし:

  • 孤立電子対は、結合電子対よりも強く反発する
  • 孤立電子対は原子核に引かれないぶん、空間を広く占有する

反発の強さは:

孤立-孤立 > 孤立-結合 > 結合-結合

となります。

その結果:

  • 孤立電子対どうしができるだけ離れようとする
  • 結合電子対(H原子)が押し縮められる

だから

109.5° → 104.5° に小さくなる

のです。


## ■ なぜ180°にならないの?

もし酸素の周りに

  • 結合電子対が2つ
  • 孤立電子対が0

なら直線(180°)になります。

実際にその構造をとる分子もあります(例:CO₂)。

しかし水は「電子対が4つ」あるため、 電子対配置は四面体が基本になります。

つまり、

原子の数ではなく、「電子対の数」で形が決まる

というのがポイントです。


## ■ まとめ

分子 電子対数 孤立電子対 基本配置 結合角
--- ---- ----- -------- ------
メタン 4 0 正四面体 109.5°
4 2 正四面体(歪む) 104.5°

## ■ さらに深い理論

VSEPRは経験則的モデルですが、 より本質的には:

  • 量子力学
  • 原子軌道の混成(sp³混成)

で説明されます。

酸素も炭素も sp³混成軌道 をとるため、 基本が四面体構造になるのです。


This year's Index

Home