Y
end
```
### 6.3 实验任务:测量加速比
实验中的 `task64.c` 要求:
1. 修改 `psum64.c`,添加计时功能
2. 分别用 1、2、4、8 个线程运行
3. 计算每个配置的加速比和效率
4. 分析为什么加速比不是线性的
**预期结果**:
| 线程数 | 执行时间 | 加速比 | 效率 |
|:------:|:--------:|:------:|:----:|
| 1 | T1 | 1.0 | 100% |
| 2 | T2 | ~1.8 | ~90% |
| 4 | T4 | ~3.2 | ~80% |
| 8 | T8 | ~5.5 | ~69% |
---
## 七、fork vs pthread_create 性能对比
### 7.1 为什么线程更快
`fork()` 创建进程时需要复制父进程的整个地址空间(虽然有写时复制优化),而 `pthread_create()` 只需要分配一个新的栈和少量元数据。
```mermaid
graph LR
subgraph "fork() 创建进程"
F1["复制页表"] --> F2["复制地址空间
(写时复制)"]
F2 --> F3["创建新的 PCB"]
F3 --> F4["开销大: ~ms 级"]
end
subgraph "pthread_create() 创建线程"
T1["分配栈空间"] --> T2["设置线程属性"]
T2 --> T3["创建线程元数据"]
T3 --> T4["开销小: ~us 级"]
end
style F4 fill:#ffcdd2
style T4 fill:#e8f5e9
```
### 7.2 实验任务:测量创建时间
实验中的 `task66.c` 要求分别测量 `fork()` 和 `pthread_create()` 的创建时间:
```c
// 测量 fork 创建时间
clock_gettime(CLOCK_MONOTONIC, &start);
for (i = 0; i < N; i++) {
if (fork() == 0) exit(0); // 子进程立即退出
wait(NULL);
}
clock_gettime(CLOCK_MONOTONIC, &end);
// 测量 pthread_create 创建时间
clock_gettime(CLOCK_MONOTONIC, &start);
for (i = 0; i < N; i++) {
pthread_create(&tid, NULL, dummy, NULL);
pthread_join(tid, NULL);
}
clock_gettime(CLOCK_MONOTONIC, &end);
```
**预期结果**:`pthread_create` 通常比 `fork` 快 10-100 倍。
---
## 八、动态线程池
### 8.1 为什么需要线程池
频繁创建和销毁线程会带来较大开销。线程池预先创建一组线程,当有任务到来时从池中取出一个线程执行,执行完毕后线程归还池中,避免反复创建销毁。
### 8.2 sbuf_t 带缓冲区的线程池
实验中的 `task67.c` 实现了一个动态线程池,核心数据结构 `sbuf_t`:
```c
typedef struct {
int *buf; // 缓冲区数组
int n; // 缓冲区容量
int front; // 队首索引
int rear; // 队尾索引
sem_t mutex; // 互斥访问
sem_t slots; // 空槽位信号量
sem_t items; // 满槽位信号量
} sbuf_t;
```
**动态调整策略**:
- **缓冲区满时翻倍**:`buf = realloc(buf, 2 * n)`
- **缓冲区空时减半**:`buf = realloc(buf, n / 2)`
```mermaid
graph TD
subgraph "线程池架构"
MAIN["主线程
接收任务"] -->|"放入缓冲区"| BUF["sbuf_t 缓冲区"]
BUF -->|"取出任务"| W1["工作线程1"]
BUF -->|"取出任务"| W2["工作线程2"]
BUF -->|"取出任务"| W3["工作线程3"]
end
subgraph "动态调整"
FULL["缓冲区满"] -->|"realloc"| DOUBLE["容量翻倍"]
EMPTY["缓冲区空"] -->|"realloc"| HALF["容量减半"]
end
style MAIN fill:#e1f5fe
style BUF fill:#fff3e0
style W1 fill:#e8f5e9
style W2 fill:#e8f5e9
style W3 fill:#e8f5e9
```
---
## 九、线程安全的数据结构
### 9.1 tickets 售票问题
参考 `实例源代码/chap6/tickets1.c`(有竞态条件)和 `tickets2.c`(使用互斥锁修复):
```c
// tickets2.c - 使用互斥锁保护售票
int tickets = 10;
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
void *counter(void *no) {
while (tickets > 0) {
pthread_mutex_lock(&mutex); // 加锁
if (tickets > 0) { // 双重检查
printf("柜台%d 卖出一张票,票号为%d\n", (int)no, tickets);
usleep(1);
tickets--;
}
pthread_mutex_unlock(&mutex); // 解锁
usleep(1);
}
}
```
**关键点**:`while` 条件检查和 `if` 条件检查都需要在锁内进行(双重检查),否则可能出现超卖。
---
## 十、实验任务概览
本讲对应 [[实验03_多线程编程]],包含以下任务:
| 任务 | 文件 | 内容 | 核心知识点 |
|------|------|------|------------|
| 任务一 | task61.c | 3 个对等线程打印姓名/学号/时间 | pthread_create/join |
| 任务二 | task62.c | 用信号量修复 badcount.c 的竞态条件 | sem_wait/sem_post |
| 任务三 | task63.c | k 个生产者 + m 个消费者,信号量同步 | 生产者-消费者模型 |
| 任务四 | task64.c | psum64.c 并行求和,测量不同线程数的加速比 | 并行计算 + 性能分析 |
| 任务五 | task66.c | 测量 fork vs pthread_create 时间 | 进程/线程创建开销对比 |
| 任务六 | task67.c | 动态线程池(sbuf_t 缓冲区满翻倍/空减半) | 线程池 + 动态数据结构 |
---
## 知识关联
- 线程是轻量级进程,理解 [[06_进程控制]] 中的 fork 有助于理解线程的优势
- 线程间的同步问题在死锁章节中会进一步讨论
- 生产者-消费者模型在 [[08_进程间通信]] 中也有管道和共享内存的实现方式
- 线程池在并发网络服务器中是核心组件
---
## 思考题
1. **互斥锁 vs 信号量**:互斥锁和二值信号量(初值为 1 的信号量)看起来功能相同,它们有什么本质区别?什么时候必须用信号量而不能用互斥锁?
2. **死锁的产生**:如果一个线程对已经加锁的互斥锁再次调用 `pthread_mutex_lock()`,会发生什么?如果两个线程分别持有对方需要的锁呢?
3. **线程安全的函数**:为什么 `printf()` 是线程安全的,而 `count++` 不是?如何判断一个函数是否线程安全?
4. **并行计算的瓶颈**:为什么 p 个线程的加速比通常达不到理想的 p 倍?哪些因素限制了并行加速?
5. **fork vs pthread**:在什么场景下应该用多进程而不是多线程?多进程模型有什么优势?
---
## 扩展阅读
- 《UNIX环境高级编程》第11章:线程
- 《深入理解计算机系统》第12章:并发编程
- 《POSIX 多线程编程指南》
- [Pthreads Tutorial (LLNL)](https://computing.llnl.gov/tutorials/pthreads/)