# 多线程开发深度教程:无栈状态机与并发编程

> **对标版本**:XTC v1.0-rc

---

## 架构概述

玄铁的并发模型是**双层架构**:OS 线程池 + 用户态调度器。

```
┌──────────────────────────────────────┐
│           XTC 编译器                  │
│  编译异步 → 状态机 IR + entry wrapper │
└──────────────┬───────────────────────┘
               │
┌──────────────▼───────────────────────┐
│       用户态调度器(主线程)           │
│  ┌──────────┐  ┌────────────────┐    │
│  │ 就绪队列  │  │  定时器堆       │    │
│  │ (FIFO)   │  │  (小顶堆)      │    │
│  └──────────┘  └────────────────┘    │
│         │              │             │
│    fiber.run()    sleep/wakeup       │
└──────────────┬───────────────────────┘
               │
┌──────────────▼───────────────────────┐
│      OS 线程池(CPU 密集型)           │
│  异步{纯计算} → xt_async_spawn       │
└──────────────────────────────────────┘
```

**核心原则**:CPU 密集型任务跑在线程池,I/O 等待型任务由调度器管理。状态机 fiber 不阻塞 OS 线程——需要等待时挂起并让出 CPU,等待条件满足后由调度器恢复。

---

## 无栈状态机

### 什么是无栈状态机

传统有栈协程(如 Go 的 goroutine)为每个协程分配独立栈(~2KB+),切换时需要保存/恢复寄存器。无栈状态机将协程编译为**状态枚举 + switch-case**,状态存储在堆分配的紧凑结构体中,切换成本为零。

```xuantie
// 用户代码
异步 {
    示("步骤1")
    等待(taskA)
    示("步骤2")
    等待(taskB)
    示("完成")
}
```

编译器将其转换为:

```llvm
; 状态结构体(堆分配,16 字节)
%AsyncState = type { i32, i64, i64 }  ; state, taskA_handle, taskB_handle

; poll 函数(无栈,由调度器调用)
define i32 @poll(%AsyncState* %self) {
entry:
  switch i32 %state, label %done [i32 0, label %s0 i32 1, label %s1 i32 2, label %s2]

s0:  ; 初始状态
  store state=1
  call @xt_print(..."步骤1")
  store taskA_handle → field[1]
  call @xt_scheduler_wait_task(taskA)
  ret i32 0  ; PENDING

s1:  ; taskA 完成后恢复
  call @xt_async_wait(taskA)      ; 验证完成
  store state=2
  call @xt_print(..."步骤2")
  store taskB_handle → field[2]
  call @xt_scheduler_wait_task(taskB)
  ret i32 0  ; PENDING

s2:  ; taskB 完成后恢复
  call @xt_async_wait(taskB)
  call @xt_print(..."完成")
  ret i32 1  ; DONE
}
```

### 编译器如何决定

编译器在编译 `异步 { ... }` 时调用 `含等标记` 扫描块体:

| 检测到 | 策略 |
|--------|------|
| 语句级 `等待` / `.收()` / `.发()` / `选` / `连` / `流.读` / `流.写` / `时.睡` | fiber 状态机(挂起点让出,不占 OS 线程) |
| 上述挂起点出现在**循环体内** | **整块降级**为线程池任务:循环体局部状态无法跨 poll 存活,降级后挂起操作按阻塞执行;编译期打印提示 |
| 含 `求` / `执` / `输`(C 侧全程阻塞,无非阻塞路径) | **整块降级**为线程池任务;编译期打印提示 |
| 无上述 | 独立函数 → 线程池 |

注:`文件.读` / `文件.写` 被显式排除在切分点之外(按阻塞调用执行);网络侧的 `连` / `流.读` / `流.写` 是 fiber 就绪切分点。

### 状态机 vs 线程池

| 特性 | 状态机 | 线程池 |
|------|--------|--------|
| 内存开销 | ~(8 + N×8) 字节 | ~4KB 栈 |
| 切换成本 | 0(调度器循环) | 上下文切换 |
| 适用场景 | I/O 等待、通道操作 | CPU 计算 |
| 并行度 | 调度器单线程 | N 核并行 |

---

## 调度器

### 事件循环

```
xt_scheduler_run():
  while running:
    now = 当前微秒时间
    tick_timers(now)      // 唤醒到期的 SLEEPING fiber
    fiber = dequeue()     // 从 READY 队列取
    if no fiber:
      if any WAITING: sleep(1ms)  // 等待任务完成通知
      else: break                  // 全部完成
    result = fiber.poll(fiber.state)
    if result == 0:        // PENDING(fiber 已自行挂起)
    else:                   // DONE
```

### Fiber 生命周期

```
  [spawn] → READY → dequeue → RUNNING
                 ↑              │
                 │    ┌─────────┼──────────┐
                 │    │         │          │
              enqueue  wait_task  sleep    ret 1
                 │    │         │          │
                 │    ▼         ▼          ▼
                 │ WAITING   SLEEPING    DONE
                 │    │         │
                 │    │    timer_tick
                 └────┴─────────┘
```

### 调度器 API(内部,用户不可直接调用)

| C 函数 | 说明 |
|--------|------|
| `xt_scheduler_init()` | 初始化调度器(`xt_init` 自动调用) |
| `xt_scheduler_run()` | 启动事件循环(main 函数末尾自动调用) |
| `xt_scheduler_spawn(state, poll)` | 创建 fiber 并入队 |
| `xt_scheduler_wait_task(task)` | 当前 fiber 挂起等待任务 |
| `xt_scheduler_wake_task(task)` | 任务完成时唤醒等待 fiber |
| `xt_scheduler_sleep_us(us)` | 当前 fiber 定时睡眠 |

---

## 通道深度用法

### 非阻塞 vs 阻塞

| 上下文 | `.发()` | `.收()` |
|--------|---------|---------|
| 普通代码 | 非阻塞(满则丢弃) | 非阻塞(空则返回 `空`) |
| `异步` 块内 | 阻塞(等待空间,条件变量) | 阻塞(等待数据,条件变量) |

阻塞版本使用条件变量,不占 OS 线程——等待的 fiber 被挂起,线程池线程释放。

### 生产者-消费者

```xuantie
设 ch = 道(4)  // 缓冲 4,平滑速率差

设 producer = 异步 {
    遍历 i 于 [1, 2, 3, 4, 5] {
        时.睡(50)  // 模拟生产耗时
        ch.发(i)
        示("生产: " & i)
    }
}

设 consumer = 异步 {
    设 sum = 0
    设 count = 0
    当 count < 5 {
        设 val = ch.收()  // 阻塞等待
        sum = sum + val
        count = count + 1
        示("消费: " & val)
    }
    示("合计: " & sum)
}

等待(producer)
等待(consumer)
```

### 扇出(Fan-Out)

多个 worker 从同一通道取任务:

```xuantie
设 tasks = 道(10)
设 results = 道(10)

// 填充任务
遍历 i 于 [1, 2, 3, 4, 5, 6] {
    tasks.发(i)
}

// 3 个 worker 竞争消费
设 worker = 函(id) {
    异步 {
        循 {
            设 t = tasks.收()
            若 t == 空 { 断 }
            时.睡(t * 20)  // 模拟处理
            results.发(t * 2)
            示("Worker " & id & " 完成: " & t)
        }
    }
}

设 w1 = worker(1)
设 w2 = worker(2)
设 w3 = worker(3)
等待(w1); 等待(w2); 等待(w3)
```

### 选择(Select)

`选(通道数组, 超时毫秒)` — 等待多个通道中第一个就绪的:

```xuantie
设 data = 道(2)
设 ctrl = 道(1)

设 sender = 异步 {
    时.睡(200)
    data.发("payload")
}

设 controller = 异步 {
    ctrl.发("stop")
}

设 selector = 异步 {
    设 idx = 选([data, ctrl], 1000)
    匹配 idx {
        0 -> { 示("数据通道就绪") }
        1 -> { 示("控制通道就绪") }
        _ -> { 示("超时") }
    }
}
```

**超时模式**:用 `选([ch], timeout)` 实现带超时的通道等待。

### 通道 vs 共享内存

| 维度 | 通道 | 共享内存 + 锁 |
|------|------|--------------|
| 数据所有权 | 转移(发送后不再使用) | 共享 |
| 竞态风险 | 无(编译器/运行时保证) | 用户负责 |
| 性能 | 条件变量等待 | 锁竞争 |
| 推荐场景 | 首选 | 性能关键路径 |

---

## 实战模式

### 流水线(Pipeline)

多个阶段通过通道串联:

```xuantie
设 raw = 道(5)
设 cooked = 道(5)

设 stage1 = 异步 { raw.发(1); raw.发(2); raw.发(3) }
设 stage2 = 异步 { 当 (设 v = raw.收()) != 空 { cooked.发(v * 10) } }
设 stage3 = 异步 { 当 (设 v = cooked.收()) != 空 { 示("结果: " & v) } }

等待(stage1); 等待(stage2); 等待(stage3)
```

### 超时重试

```xuantie
设 ch = 道(1)
设 op = 异步 {
    设 attempt = 0
    当 attempt < 3 {
        attempt = attempt + 1
        示("尝试 " & attempt)
        设 idx = 选([ch], 1000)
        若 idx >= 0 { 示("成功"); 断 }
        示("超时,重试...")
    }
}
```

### 并发等待多个任务

```xuantie
设 t1 = 异步 { 时.睡(200); 返 "A" }
设 t2 = 异步 { 时.睡(100); 返 "B" }
设 t3 = 异步 { 时.睡(300); 返 "C" }

设 collector = 异步 {
    设 ch = 道(3)
    设 runner = 函(t, label) {
        异步 { 等待(t); ch.发(label) }
    }
    runner(t1, "A")
    runner(t2, "B")
    runner(t3, "C")
    设 results = []
    设 i = 0; 当 i < 3 { results.追加(ch.收()); i = i + 1 }
    示("全部完成: " & results.连接(","))
}
等待(collector)
```

### 计数信号量(限制并发数)

```xuantie
设 sem = 道(3)  // 最多 3 并发

设 limitedTask = 函(id) {
    异步 {
        sem.发(1)         // 获取许可
        示("任务 " & id & " 开始")
        时.睡(id * 100)
        示("任务 " & id & " 完成")
        设 _ = sem.收()   // 释放许可
    }
}

遍历 i 于 [1, 2, 3, 4, 5] {
    limitedTask(i)
}
```

---

## 性能与调试

### Fiber 开销

| 指标 | 数值 |
|------|------|
| 每 fiber 内存 | ~(8 + N×8) 字节(N=等待点数) |
| 创建/销毁 | malloc/free 一次 |
| 切换(poll) | 函数调用 + switch 跳转 |
| 最大 fiber 数 | 65535(`XT_MAX_FIBERS`,编译期常量) |
| 最大定时器 | 8192(`XT_TIMER_HEAP_SIZE`,编译期常量;耗尽显式报错,不静默丢 fiber) |

### 常见问题

**Q: 为什么我的异步块没有并发执行?**
A: 检查是否调用了 `等待(task)` —— 未等待的任务仍在执行。程序末尾的 `xt_scheduler_run()` 会自动调度所有 fiber。

**Q: 状态机中能使用 `时.睡` 吗?**
A: 能,且不阻塞线程:状态机内的 `时.睡` 是挂起点,挂到调度器定时器堆(`xt_scheduler_sleep_us`)后让出。实测 64 个并发 `时.睡(300)` 总耗时 300ms——若每个都占住 worker 线程则需约 1800ms。但若该异步块因"循环体内有挂起点"或"含 `求`/`执`/`输`"被整块降级为线程池任务,块内执行的是阻塞版 `xt_time_sleep`,会占住一个 worker 线程。

**Q: 如何避免死锁?**
A: (1) 通道容量 > 0 作为缓冲;(2) 避免循环等待(A 等 B,B 等 A);(3) 使用 `选` 的超时机制。

### 调试技巧

```xuantie
// 打印 fiber 信息(开发中)
// 当前可用:通过输出来追踪状态转换
异步 {
    示("[DEBUG] s0 开始")
    等待(task)
    示("[DEBUG] s1 恢复")
}
```

---

## 已知限制与路线图

### 当前限制

| 限制 | 影响 | 预计修复 |
|------|------|---------|
| 循环体内的挂起点会降级 | `等待`/`收`/`发`/`选`/`时.睡`/`连`/`读`/`写` 出现在循环体里时,整个异步块降级为线程池任务(运行期独占一个 worker 线程),失去 fiber 轻量调度;编译期会打印提示 | 设计取舍:循环体局部状态无法跨 poll 恢复 |
| 同一条语句内的重放 | `等待` 所在的那条语句中、`等待` 之前的部分在恢复后会重复执行(`设 x = 副作用() + 等待(t)` 会让 `副作用()` 跑两次);跨语句不重放 | P6b |
| 外层可变变量按"任务开始执行时"读取 | 块内读到的不是创建时刻的值,而是任务真正开始执行时的值——循环里批量起任务会全部读到终值 | 语义规则,见 [异步](关键字/异步.md) 的捕获说明;`遍历` 循环变量本身逐迭代正确 |
| 阻塞原语不可调度 | `求` / `执` / `输`,以及 `文件.读` / `文件.写`,走阻塞实现;含它们的异步块整块降级为线程池任务 | P6b(网络侧已是 fiber 就绪切分点,文件侧未接入) |
| 无 fiber 取消 | 无法终止等待中的 fiber,也没有超时取消接口 | P7 |
| 容量为编译期常量 | fiber 池 65535、定时器堆 8192;耗尽会显式报错退出 | P7(改 `runtime/xt_scheduler.h` 重编即可调) |

> 调度器状态由互斥锁保护(`g_sched_mutex`),线程池自带互斥锁与条件变量;worker 数按 CPU 核数自动确定(上限 64)。尚未做过专门的竞态压力审计。

### 路线图

| 阶段 | 内容 | 状态 |
|------|------|------|
| P1 | `含等标记` 扫描器 | ✅ |
| P2 | 单等待状态机 | ✅ |
| P3 | 多等待状态机 | ✅ |
| P4 | 通道 + `选` + 阻塞收发 | ✅ |
| P5 | 调度器原型(就绪队列 + 定时器堆 + 事件驱动唤醒) | ✅ |
| P6 | 编译路径自动路由:线性挂起走 fiber 状态机,循环内挂起与阻塞原语整块降级线程池(均带编译期提示) | ✅ |
| P6b | 系统 IO 集成(文件/网络 → 状态机) | 部分完成:网络侧 `连`/`流.读`/`流.写` 已是 fiber 就绪切分点;文件 IO 仍为阻塞路径 |
| P7 | 性能调优(work-stealing、NUMA、CPU affinity) | 规划中 |

---

## 完整示例:并发爬虫

```xuantie
// 简易并发爬虫——展示异步+通道+选的实际应用
设 urls = ["http://example.com", "http://example.org"]
设 results = 道(5)
设 done = 道(1)

设 crawl = 函(url) {
    异步 {
        设 resp = 求(url)
        若 resp.成功 {
            results.发(url & " → " & resp.值.截取(0, 100))
        } 否 {
            results.发(url & " → 失败")
        }
    }
}

// 并发发起
遍历 u 于 urls {
    crawl(u)
}

// 收集结果
设 collector = 异步 {
    设 count = 0
    当 count < urls.长度 {
        设 idx = 选([results], 5000)
        若 idx >= 0 {
            设 r = results.收()
            示(r)
            count = count + 1
        } 否 {
            示("超时,停止等待")
            断
        }
    }
    done.发("完成")
}
等待(collector)
```