# 线程创建的详细过程 —— 从 clone() 到"主线程什么时候返回"

> 上一篇 [process-creation.md](/concepts/process/process-creation.md) 讲了进程怎么"复制"出来（`fork` + 少量 clone flags）。本篇聚焦**线程**：它和进程本质上都是内核里的"调度实体（task）"，走的都是 `clone()`，区别只在**共享多少**。本篇讲透线程创建的全过程，并重点回答一个常见困惑：**"创建一个线程，主线程什么时候返回？"** 全篇呼应 demo 程序 [../../demos/cpu-demo/main.cpp](/demos/cpu-demo/main.cpp)（`std::thread` + `detach()`）。

## 一、线程 vs 进程的本质区别

一句话：**进程是"资源的容器"，线程是"调度的单位"。** 同一进程里的多个线程**共享**绝大部分资源，只保留极少数"每个执行流必须独占"的东西。

| 资源 | 进程间（fork） | 同进程的线程间 |
|------|---------------|---------------|
| 虚拟地址空间（代码段/数据段/堆） | **各自独立**（copy-on-write 复制） | **共享**（同一份） |
| 全局变量、静态变量 | 各自一份 | **共享**（改一个都看得见） |
| 打开的文件描述符表（fd） | fork 时复制一份（之后独立） | **共享**（一个线程 open 的 fd，别的线程能用） |
| 当前工作目录、文件系统信息 | 独立 | **共享** |
| 信号处理函数（handler） | 独立 | **共享** |
| **栈（stack）** | 独立 | **各自独立**（每线程一个栈） |
| **寄存器 / 程序计数器（PC）** | 独立 | **各自独立**（各跑各的指令流） |
| **TLS（线程本地存储）** | —— | **各自独立**（`errno`、`__thread` 变量） |
| **内核调度实体 / tid** | 独立 | **各自独立**（每线程一个 tid，可被独立调度到不同核） |

看得出规律：**"要一起看到同一份数据"的东西共享（地址空间、全局、fd）；"每个执行流跑指令必须独占"的东西独立（栈、寄存器、TLS、tid）。**

```plantuml
@startuml
skinparam shadowing false
skinparam rectangle {
  BackgroundColor #E3F2FD
  BorderColor     #1976D2
}
rectangle "进程（一个地址空间）" {
  rectangle "共享区\n代码段 / 全局变量 / 堆 / 打开的 fd / 信号处理" as SHARE #FFF9C4
  rectangle "主线程\n独立栈 + 寄存器 + TLS + tid" as T1 #C8E6C9
  rectangle "工作线程\n独立栈 + 寄存器 + TLS + tid" as T2 #C8E6C9
}
T1 --> SHARE : 读写
T2 --> SHARE : 读写
note bottom of SHARE : 两个线程改同一个全局变量\n互相立刻可见（所以才需要锁 / 原子操作）
@enduml
```

## 二、pthread_create / std::thread 底层是 clone()

`std::thread`（C++）→ 底层调用 `pthread_create`（glibc / NPTL）→ 最终发起 **`clone()`** 系统调用。线程和进程创建**用的是同一个系统调用**，差别全在传给 `clone()` 的 **flags**：flags 里每一位表示"这个资源要不要和父任务**共享**"。

- **fork**（造进程）：只带很少的共享位，绝大部分资源独立（因为父子进程之间基本不共享什么信息）。
- **pthread_create**（造线程）：带**一大堆 `CLONE_*`** 共享位，几乎什么都共享。

线程创建典型 flags：

| flag | 含义（打开=共享） |
|------|------------------|
| `CLONE_VM` | 共享虚拟地址空间（同一份内存，最关键） |
| `CLONE_FILES` | 共享打开文件描述符表 |
| `CLONE_FS` | 共享文件系统信息（cwd、umask、root） |
| `CLONE_SIGHAND` | 共享信号处理函数表 |
| `CLONE_THREAD` | 归入**同一个线程组**（同一 `PID`，各自不同 `tid`） |
| `CLONE_SETTLS` | 设置新线程的 TLS 寄存器 |
| `CLONE_PARENT_SETTID` / `CLONE_CHILD_CLEARTID` | 配合 glibc 管理 tid、线程退出时清理 |

> 对比记忆（呼应 [process-creation.md](/concepts/process/process-creation.md) 的 clone 表）：**`fork` ≈ clone(少量 flags)，几乎啥都不共享；`pthread_create` ≈ clone(CLONE_VM|CLONE_FILES|CLONE_FS|CLONE_SIGHAND|CLONE_THREAD|...)，几乎啥都共享。** 同一个 clone，flags 不同，造出来的就是"进程"还是"线程"。

在发起 `clone()` **之前**，glibc/NPTL 还替我们干了两件苦活：

1. **分配线程栈**：默认约 8MB（可通过 `pthread_attr_setstacksize` 调整），用 `mmap` 从地址空间里划一块，作为新线程的独立栈。
2. **设置 TLS**：为新线程准备线程本地存储区，并通过 `CLONE_SETTLS` 让内核把 TLS 寄存器（x86-64 上的 `fs`）指过去。

```plantuml
@startuml
skinparam shadowing false
skinparam sequence {
  ParticipantBackgroundColor #E3F2FD
  ParticipantBorderColor     #1976D2
  ArrowColor                 #1565C0
  LifeLineBorderColor        #90A4AE
}
participant "主线程" as M
participant "glibc/NPTL" as G
participant "内核" as K
participant "新线程" as N
M -> G : pthread_create(fn)
G -> G : mmap 分配新线程栈(约8MB)\n准备 TLS
G -> K : clone(CLONE_VM|CLONE_FILES|CLONE_FS\n|CLONE_SIGHAND|CLONE_THREAD|CLONE_SETTLS|...)
K -> N : 创建可调度的新 task(新 tid)\n栈指针指向新栈, PC 指向启动例程
K --> G : clone 返回
G --> M : pthread_create 返回(立即!)
note over M : 主线程继续往下跑
note over N : 与主线程**共享地址空间**\n但用**自己的栈/寄存器/TLS**\n由调度器择机开始执行 fn
@enduml
```

## 三、【重点】"创建一个线程，主线程什么时候返回？"

这是最容易搞错的点，拆开讲清楚。

### 3.1 pthread_create/clone 是异步的 —— 调用一返回就继续

`pthread_create`（底层 `clone`）是**异步**的：

- **返回时机** = `clone` 系统调用完成、内核已经**创建好一个可被调度的新线程**（分配了 tid、栈指针、入口 PC）。
- 返回**不代表**新线程已经开始执行，**更不代表**新线程跑完。**甚至新线程可能一条指令都还没跑**，主线程就已经拿到返回值、继续往下走了。

所以答案是：**主线程在"内核把新线程建好并可调度"的那一刻就立刻返回，继续执行 `pthread_create` 之后的代码——它不等新线程开始跑，也不等它跑完。**

### 3.2 此后两者并发，谁先跑不确定

`pthread_create` 返回后，主线程和新线程就是**两个独立的可调度实体**，由 **CPU 调度器**决定：

- 谁先跑（可能主线程先跑一大段，也可能新线程立刻被调度）；
- 跑在哪个核（多核上可能真正**并行**）；
- 怎么交替。

**这个顺序不确定、不可依赖。** 想要确定的先后，必须自己用 `join()` / 条件变量 / 信号量等同步手段。

```plantuml
@startuml
skinparam shadowing false
skinparam sequence {
  ParticipantBackgroundColor #E3F2FD
  ParticipantBorderColor     #1976D2
  ArrowColor                 #C62828
  LifeLineBorderColor        #90A4AE
}
participant "主线程" as M
participant "内核/调度器" as K
participant "新线程" as N
M -> K : clone(...)
K -> N : 建好新 task(可调度)
K --> M : **立即返回**
activate M
M -> M : 继续执行后续代码\n(不等新线程)
activate N
note over M, N : 此后两者**并发**\n谁先跑、跑哪个核由调度器决定
N -> N : 某时刻被调度\n才真正开始执行 fn
deactivate N
deactivate M
@enduml
```

### 3.3 想"等"新线程：join() vs detach()

主线程若要**等**新线程，必须**显式**调用 `join()`；否则默认就是不等。

| 操作 | 语义 | 主线程行为 | 资源回收 |
|------|------|-----------|---------|
| `join()` | 汇合 | **阻塞等待**目标线程结束后才继续 | join 时回收线程资源 |
| `detach()` | 分离 | **不等**，各跑各的，放手不管 | 线程结束时自动回收 |
| 都不调用 | —— | `std::thread` 析构时若仍 joinable，直接 `std::terminate()` 崩溃 | —— |

demo 里用的是 **`detach()`**（[../../demos/cpu-demo/main.cpp](/demos/cpu-demo/main.cpp) 第 126 行 `work_thread.detach();`）：主线程把工作线程"放手"，然后自己进入每秒采样打印的循环，两者并发跑——工作线程狂消耗 CPU，主线程负责观测。

### 3.4 detach 的关键后果：主线程 return，整个进程就没了

这是 demo 结构的核心原因，必须讲透：

- **进程的生命周期 ≠ 主线程 return 就等新线程。** 一旦 `main()` 里的主线程 `return`（或调用 `exit()`），**整个进程连同它所有线程（包括还在跑的 detached 线程）会被一起销毁**——因为进程地址空间被拆掉了，寄居其中的线程也就没了。
- 所以，如果 demo 主线程在 `detach()` 之后直接 `return`，工作线程可能一条业务指令都还没来得及跑，进程就退出了，什么现象都观测不到。
- **这正是 [../../demos/cpu-demo/main.cpp](/demos/cpu-demo/main.cpp) 里主线程 `detach()` 后进入 `while(true){ sleep(1); ... }` 无限循环、而不是 `return` 的原因**：用主线程"不退出"来撑住整个进程存活，让 detached 工作线程有机会一直跑下去，供 `top`/`pidstat` 观测。

> 对照：如果改用 `join()`，主线程会阻塞在 join 处直到工作线程结束——但 demo 的工作线程本身是 `while(true)` 死循环，join 会永远阻塞，采样循环也就跑不起来了。所以 demo 选 `detach()` + 主线程自己循环，是刻意设计。

## 四、线程栈、TLS、线程 ID

- **线程栈**：每线程一个独立栈，Linux 上默认约 **8MB**（`ulimit -s` 查看主线程栈上限；子线程栈由 pthread 分配）。可用 `pthread_attr_setstacksize` 或 `std::thread` 之外的属性调整。栈是线程独立的根本原因——各自的函数调用链、局部变量互不干扰。
- **TLS（线程本地存储）**：像 `errno` 就是 TLS，每个线程有自己的一份，A 线程的系统调用出错不会污染 B 线程的 `errno`；C/C++ 里 `__thread` / `thread_local` 变量也是 TLS。
- **线程 ID 的两套概念**：
  - `gettid()`：返回**内核视角的线程 ID（tid）**，全系统唯一，就是 `/proc/$PID/task/` 下看到的那个号，也是 `top -H`、`ps -T` 显示的号。主线程的 tid 恰好等于进程的 PID。
  - `pthread_self()`：返回 **glibc 用户态视角的 pthread_t 句柄**，只在本进程内有意义，用于 `pthread_join`/`pthread_cancel` 等，**不等于** tid。
  - 一句话：**排查/观测看 `gettid`（tid），代码里管理线程用 `pthread_self`（pthread_t）。**

## 五、观测：怎么看到线程被创建

| 手段 | 命令 | 看什么 |
|------|------|--------|
| 跟踪系统调用 | `strace -f ./cpu_demo` | 抓到 `clone(...)` 且 flags 里带 `CLONE_THREAD\|CLONE_VM\|...`（对比 fork 的 clone flags 少得多） |
| 看进程内的线程 | `ps -T -p <PID>` | 每个线程一行，`SPID` 列就是 tid |
| top 展开线程 | `top -H -p <PID>` | 按线程显示 CPU，能看到工作线程吃满一个核、主线程几乎不耗 CPU |
| /proc | `ls /proc/<PID>/task/` | 每个子目录名就是一个线程的 tid |

跑 demo 时用 `strace -f` 能清楚看到主线程发起带一大堆 `CLONE_*` 的 `clone()` 造出工作线程，再用 `top -H` / `ps -T` 看到进程里有两个线程、工作线程在狂吃 CPU。

延伸阅读：[../cpu/top.md](/tools/cpu/top.md)（含 `-H` 线程视图）、[../cpu/pidstat.md](/tools/cpu/pidstat.md)（`-t` 按线程统计）、[../code/strace.md](/tools/code/strace.md)（`-f` 跟踪 clone）、[fork-and-threads.md](/concepts/process/fork-and-threads.md)（fork 与线程的取舍对比）、[thread-affinity.md](/concepts/process/thread-affinity.md)（把线程钉到指定 CPU 核）。

## 一句话总结

**线程创建走的是带一大堆 `CLONE_*` 共享 flags 的 `clone()`，与 fork 只是 flags 多少之别；`pthread_create` 是异步的——内核一把新线程建成可调度，主线程就立刻返回继续跑，绝不等新线程开始或结束，想等就 `join()`、放手就 `detach()`，而一旦主线程 return/exit 整个进程连同 detached 线程一起销毁（这正是 [../../demos/cpu-demo/main.cpp](/demos/cpu-demo/main.cpp) 主线程 detach 后进死循环 sleep 而不 return 的原因）。**
