Files
obsidian/操作系统/07_多线程编程/07_多线程编程.md

841 lines
24 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 第07讲多线程编程
> **本节目标**:理解线程的概念与优势,掌握 POSIX 线程 APIpthread理解竞态条件的成因与解决方案互斥锁、信号量能够实现并行计算和生产者-消费者模型。
## 前置知识
- [[06_进程控制]] -- 进程的概念与创建
- [[08_进程间通信]] -- 进程间通信方式(与线程通信对比)
---
## 一、线程概念
### 1.1 什么是线程
**线程Thread** 是进程内的一个执行单元。一个进程可以包含多个线程,这些线程共享进程的地址空间,但每个线程拥有自己独立的栈和寄存器状态。
**生活比喻**
- **进程** = 一家公司(独立的办公室、财务系统、人员编制)
- **线程** = 公司里的员工(共享办公室和财务系统,但各有各的工作任务)
### 1.2 进程 vs 线程
```mermaid
graph TB
subgraph "多进程模型"
P1["进程1"] --> M1["独立的地址空间"]
P2["进程2"] --> M2["独立的地址空间"]
M1 -.->|"IPC 通信<br/>(管道/共享内存)"| M2
end
subgraph "多线程模型"
T1["线程1"] --> SM["共享的地址空间"]
T2["线程2"] --> SM
T3["线程3"] --> SM
SM --> S1["代码段 (共享)"]
SM --> S2["数据段 (共享)"]
SM --> S3["堆 (共享)"]
SM --> S4["栈 (各自独立)"]
end
style P1 fill:#e1f5fe
style P2 fill:#e1f5fe
style T1 fill:#e8f5e9
style T2 fill:#e8f5e9
style T3 fill:#e8f5e9
style SM fill:#fff3e0
```
| 对比维度 | 进程 | 线程 |
|----------|------|------|
| 地址空间 | 独立(互不可见) | 共享(可见彼此的变量) |
| 创建开销 | 大(复制页表、地址空间) | 小(只创建栈和寄存器) |
| 切换开销 | 大(切换页表、刷新 TLB | 小(只切换栈指针和寄存器) |
| 通信方式 | 需要 IPC 机制(管道、共享内存等) | 直接读写共享变量 |
| 安全性 | 高(地址空间隔离) | 低(需要同步机制保护) |
| 典型应用 | 独立的程序(如浏览器和音乐播放器) | 同一程序内的并发任务 |
### 1.3 线程的共享与私有
| 资源 | 共享/私有 | 说明 |
|------|-----------|------|
| 代码段 | 共享 | 所有线程执行相同的代码 |
| 数据段(全局变量) | 共享 | 所有线程可见同一份全局变量 |
| 堆malloc 分配的内存) | 共享 | 任一线程分配的内存,其他线程可访问 |
| 栈 | **私有** | 每个线程有自己的栈,存放局部变量 |
| 寄存器状态 | **私有** | 每个线程有自己的程序计数器、栈指针等 |
| 信号处理 | 共享 | 信号发送给进程,任意线程可处理 |
---
## 二、POSIX 线程 API
### 2.1 核心函数
| 函数 | 作用 | 说明 |
|------|------|------|
| `pthread_create()` | 创建新线程 | 类似 `fork()`,但开销小得多 |
| `pthread_join()` | 等待线程结束 | 类似 `wait()`,阻塞调用线程 |
| `pthread_exit()` | 终止当前线程 | 类似 `exit()`,但只终止当前线程 |
| `pthread_self()` | 获取当前线程 ID | 类似 `getpid()` |
| `pthread_detach()` | 分离线程 | 线程结束后自动释放资源 |
| `pthread_cancel()` | 取消线程 | 请求终止另一个线程 |
### 2.2 pthread_create 详解
```c
int pthread_create(
pthread_t *tid, // 输出参数:新线程的 ID
const pthread_attr_t *attr, // 线程属性(通常传 NULL
void *(*start_routine)(void*), // 线程函数(新线程从这里开始执行)
void *arg // 传递给线程函数的参数
);
// 返回值:成功返回 0失败返回错误码
```
```mermaid
graph LR
A["主线程调用<br/>pthread_create()"] --> B["操作系统创建新线程"]
B --> C["新线程执行<br/>start_routine(arg)"]
C --> D["线程函数返回"]
D --> E["主线程调用<br/>pthread_join() 回收"]
style A fill:#e1f5fe
style C fill:#e8f5e9
style E fill:#fff3e0
```
### 2.3 线程创建示例
参考 `实例源代码/chap6/pthread1.c`
```c
#include "wrapper.h"
void *peertask(void *vargp);
int main()
{
pthread_t tid;
int i;
pthread_create(&tid, NULL, peertask, NULL); // 创建新线程
for (i = 0; i < 2; i++) {
printf("main thread looped %d times\n", i);
usleep(10);
}
pthread_join(tid, NULL); // 等待新线程结束
exit(0);
}
void *peertask(void *vargp) // 线程函数
{
int i;
for (i = 0; i < 4; i++) {
printf("peer thread looped %d times\n", i);
usleep(10);
}
return NULL;
}
```
**输出示例**(顺序不确定,因为主线程和新线程并发执行):
```
main thread looped 0 times
peer thread looped 0 times
main thread looped 1 times
peer thread looped 1 times
peer thread looped 2 times
peer thread looped 3 times
```
**关键点**
- `pthread_create` 创建新线程后,两个线程**并发执行**
- `pthread_join` 阻塞主线程,直到新线程结束
- 输出顺序取决于调度器,每次运行可能不同
- 编译时需要加 `-lpthread` 链接 pthread 库
---
## 三、线程安全问题 -- 竞态条件
### 3.1 什么是竞态条件
**竞态条件Race Condition** 是指多个线程同时访问和修改共享变量,最终结果取决于线程执行的精确时序,导致程序行为不可预测。
### 3.2 竞态条件演示
参考 `实例源代码/chap6/threadrace.c`
```c
#include "wrapper.h"
#define N 4
void *thread(void *vargp);
int main()
{
pthread_t tid[N];
int i;
for (i = 0; i < N; i++)
pthread_create(&tid[i], NULL, thread, &i); // 传递 i 的地址
for (i = 0; i < N; i++)
pthread_join(tid[i], NULL);
exit(0);
}
void *thread(void *vargp)
{
usleep(1);
int myid = *((int *)vargp); // 读取线程 ID
printf("Hello from thread %d\n", myid);
return NULL;
}
```
**问题分析**:所有线程共享同一个变量 `i` 的地址。当线程读取 `*vargp` 时,`i` 的值可能已经被主线程的循环修改了。
**可能出现的输出**
```
Hello from thread 2
Hello from thread 2
Hello from thread 3
Hello from thread 3
```
### 3.3 竞态条件的时序分析
```mermaid
sequenceDiagram
participant 主线程
participant 线程0
participant 线程1
participant 线程2
participant 线程3
主线程->>线程0: pthread_create(&i)
主线程->>主线程: i++ (i=1)
主线程->>线程1: pthread_create(&i)
主线程->>主线程: i++ (i=2)
主线程->>线程2: pthread_create(&i)
主线程->>主线程: i++ (i=3)
主线程->>线程3: pthread_create(&i)
Note over 线程0: 读取 *vargp但 i 已经是 3
Note over 线程1: 读取 *vargpi 也是 3
Note over 线程2: 读取 *vargpi 也是 3
Note over 线程3: 读取 *vargpi 是 3
线程0-->>主线程: Hello from thread 3 (错误!)
线程1-->>主线程: Hello from thread 3 (错误!)
线程2-->>主线程: Hello from thread 3 (错误!)
线程3-->>主线程: Hello from thread 3
```
### 3.4 经典竞态badcount.c
参考 `实例源代码/chap6/badcount.c`
```c
#include "wrapper.h"
int count = 0;
void *increase(void *arg) {
unsigned int i, niters = (unsigned int)arg;
for (i = 0; i < niters; i++)
count++; // 非原子操作!
return NULL;
}
void *decrease(void *arg) {
unsigned int i, niters = (unsigned int)arg;
for (i = 0; i < niters; i++)
count--; // 非原子操作!
return NULL;
}
int main(int argc, char **argv) {
unsigned int niters = atoll(argv[1]);
pthread_t tid1, tid2;
pthread_create(&tid1, NULL, increase, (void*)niters);
pthread_create(&tid2, NULL, decrease, (void*)niters);
pthread_join(tid1, NULL);
pthread_join(tid2, NULL);
if (count != 0)
printf("Error! cnt=%d\n", count); // 期望为0但实际可能不为0
else
printf("Correct cnt=%d\n", count);
exit(0);
}
```
**为什么 `count++` 和 `count--` 不是原子操作?**
```mermaid
graph LR
subgraph "count++ 的三条指令"
A1["LOAD count -> 寄存器"] --> A2["寄存器 = 寄存器 + 1"]
A2 --> A3["STORE 寄存器 -> count"]
end
subgraph "count-- 的三条指令"
B1["LOAD count -> 寄存器"] --> B2["寄存器 = 寄存器 - 1"]
B2 --> B3["STORE 寄存器 -> count"]
end
style A1 fill:#e1f5fe
style A2 fill:#fff3e0
style A3 fill:#ffcdd2
style B1 fill:#e1f5fe
style B2 fill:#fff3e0
style B3 fill:#ffcdd2
```
当两个线程的 LOAD/STORE 指令交错执行时,就会出现丢失更新的问题。
---
## 四、互斥锁Mutex
### 4.1 互斥锁的原理
互斥锁Mutex保证同一时刻只有一个线程可以进入**临界区**(访问共享资源的代码段)。其他试图获取已锁定的互斥锁的线程会被阻塞,直到锁被释放。
```mermaid
sequenceDiagram
participant 线程1
participant 互斥锁
participant 线程2
participant 共享变量
线程1->>互斥锁: pthread_mutex_lock()
Note over 互斥锁: 锁状态:已锁定
线程1->>共享变量: 读取 count (0)
线程2->>互斥锁: pthread_mutex_lock()
Note over 互斥锁: 锁已被占用线程2 阻塞
线程1->>共享变量: count = count + 1 = 1
线程1->>互斥锁: pthread_mutex_unlock()
Note over 互斥锁: 锁状态:已解锁
互斥锁-->>线程2: 被唤醒,获得锁
线程2->>共享变量: 读取 count (1)
线程2->>共享变量: count = count - 1 = 0
线程2->>互斥锁: pthread_mutex_unlock()
Note over 共享变量: count = 0 (正确!)
```
### 4.2 互斥锁 API
| 函数 | 作用 |
|------|------|
| `pthread_mutex_init()` | 初始化互斥锁 |
| `pthread_mutex_lock()` | 加锁(阻塞等待) |
| `pthread_mutex_trylock()` | 尝试加锁(非阻塞) |
| `pthread_mutex_unlock()` | 解锁 |
| `pthread_mutex_destroy()` | 销毁互斥锁 |
**初始化方式**
```c
// 方式1静态初始化
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
// 方式2动态初始化
pthread_mutex_t mutex;
pthread_mutex_init(&mutex, NULL);
```
### 4.3 互斥锁修复竞态
参考 `实例源代码/chap6/mutex1.c`
```c
// mutex1.c - 使用互斥锁保护共享变量
#include "wrapper.h"
int count = 0;
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
void *thread_fun(void *no) {
pthread_mutex_lock(&mutex); // 加锁 -- 进入临界区
count++;
printf("thread %d, count = %d\n", (int)no, count);
pthread_mutex_unlock(&mutex); // 解锁 -- 离开临界区
}
int main() {
pthread_t thread_id1, thread_id2;
Pthread_create(&thread_id1, NULL, thread_fun, (void*)1);
Pthread_create(&thread_id2, NULL, thread_fun, (void*)2);
Pthread_join(thread_id1, NULL);
Pthread_join(thread_id2, NULL);
Pthread_exit(0);
}
```
**输出**(保证正确):
```
thread 1, count = 1
thread 2, count = 2
```
### 4.4 另一种修复方式:避免共享
参考 `实例源代码/chap6/norace.c`,通过为每个线程分配独立的内存来避免共享:
```c
// norace.c - 使用动态分配避免竞态
for (i = 0; i < N; i++) {
ptr = malloc(sizeof(int)); // 为每个线程分配独立的内存
*ptr = i;
pthread_create(&tid[i], NULL, thread, ptr);
}
```
**原理**:每个线程拥有自己独立的 `int` 变量,不再共享 `i`,从根本上消除了竞态条件。
---
## 五、信号量Semaphore
### 5.1 信号量的概念
信号量是一种比互斥锁更通用的同步机制,由 Dijkstra 提出。信号量是一个非负整数计数器,支持两个原子操作:
- **P 操作**`sem_wait`):计数器减 1如果计数器 < 0 则阻塞
- **V 操作**`sem_post`):计数器加 1如果有等待者则唤醒
```mermaid
graph LR
subgraph "信号量 S"
S["S = 计数器"]
end
P["sem_wait (P操作)<br/>S--"] -->|S >= 0| OK["允许通过"]
P -->|S < 0| BLOCK["阻塞等待"]
V["sem_post (V操作)<br/>S++"] --> WAKE["唤醒等待者"]
style S fill:#fff3e0
style OK fill:#e8f5e9
style BLOCK fill:#ffcdd2
```
### 5.2 信号量 API
| 函数 | 作用 |
|------|------|
| `sem_init(&sem, pshared, value)` | 初始化信号量value 为初始值 |
| `sem_wait(&sem)` | P 操作等待信号量S--,若 S<0 则阻塞) |
| `sem_post(&sem)` | V 操作释放信号量S++,唤醒等待者) |
| `sem_getvalue(&sem, &val)` | 获取信号量当前值 |
| `sem_destroy(&sem)` | 销毁信号量 |
### 5.3 互斥锁 vs 信号量
| 特性 | 互斥锁 (Mutex) | 信号量 (Semaphore) |
|------|----------------|-------------------|
| 计数范围 | 0 或 1二值 | 0 到 N计数 |
| 用途 | 互斥(保护临界区) | 同步(协调执行顺序) |
| 加锁/解锁者 | 谁加锁谁解锁 | 一个线程 sem_wait另一个 sem_post |
| 典型场景 | 保护共享变量 | 生产者-消费者、资源池 |
### 5.4 生产者-消费者问题
生产者-消费者是经典的同步问题:生产者向缓冲区放入数据,消费者从缓冲区取出数据。需要解决:
- 缓冲区满时生产者必须等待
- 缓冲区空时消费者必须等待
- 同一时刻只有一个线程访问缓冲区
**使用信号量解决**
```c
#include <semaphore.h>
#define N 10 // 缓冲区大小
int buf[N];
int in = 0, out = 0;
sem_t mutex; // 互斥信号量,初值 1
sem_t empty; // 空槽位数,初值 N
sem_t full; // 满槽位数,初值 0
void *producer(void *arg) {
while (1) {
int item = produce_item();
sem_wait(&empty); // 等待空槽位 (empty--)
sem_wait(&mutex); // 进入临界区
buf[in] = item;
in = (in + 1) % N;
sem_post(&mutex); // 离开临界区
sem_post(&full); // 增加满槽位 (full++)
}
}
void *consumer(void *arg) {
while (1) {
sem_wait(&full); // 等待满槽位 (full--)
sem_wait(&mutex); // 进入临界区
int item = buf[out];
out = (out + 1) % N;
sem_post(&mutex); // 离开临界区
sem_post(&empty); // 增加空槽位 (empty++)
consume_item(item);
}
}
```
**生产者-消费者流程图**
```mermaid
graph TD
subgraph "生产者"
P1["生产数据"] --> P2["sem_wait(empty)<br/>等待空槽位"]
P2 --> P3["sem_wait(mutex)<br/>进入临界区"]
P3 --> P4["放入缓冲区"]
P4 --> P5["sem_post(mutex)<br/>离开临界区"]
P5 --> P6["sem_post(full)<br/>通知消费者"]
P6 --> P1
end
subgraph "消费者"
C1["sem_wait(full)<br/>等待满槽位"] --> C2["sem_wait(mutex)<br/>进入临界区"]
C2 --> C3["取出数据"]
C3 --> C4["sem_post(mutex)<br/>离开临界区"]
C4 --> C5["sem_post(empty)<br/>通知生产者"]
C5 --> C6["处理数据"]
C6 --> C1
end
P6 -.->|"full++ 唤醒"| C1
C5 -.->|"empty++ 唤醒"| P2
style P1 fill:#e1f5fe
style C6 fill:#e8f5e9
```
### 5.5 实验任务:修复 badcount.c
实验中的 `task62.c` 要求使用信号量修复 `badcount.c` 的竞态条件:
```c
#include <semaphore.h>
sem_t mutex;
sem_init(&mutex, 0, 1); // 互斥信号量,初值 1
void *increase(void *arg) {
for (i = 0; i < niters; i++) {
sem_wait(&mutex); // P 操作
count++;
sem_post(&mutex); // V 操作
}
}
void *decrease(void *arg) {
for (i = 0; i < niters; i++) {
sem_wait(&mutex); // P 操作
count--;
sem_post(&mutex); // V 操作
}
}
```
---
## 六、并行计算
### 6.1 并行求和问题
参考 `实例源代码/chap6/psum64.c`,将一个大数组的求和任务分配给多个线程并行计算:
```c
#include "wrapper.h"
#define MAXTHREADS 32
unsigned long long psum[MAXTHREADS]; // 每个线程的部分和
unsigned long long nelems_per_thread; // 每个线程处理的元素数
void *sum(void *vargp) {
int myid = *((int *)vargp);
unsigned long long start = myid * nelems_per_thread;
unsigned long long end = start + nelems_per_thread;
unsigned long long i, lsum = 0;
for (i = start; i < end; i++)
lsum += i;
psum[myid] = lsum; // 将部分和存入全局数组
return NULL;
}
int main(int argc, char **argv) {
unsigned long long i, nelems, nthreads, result = 0;
pthread_t tid[MAXTHREADS];
int myid[MAXTHREADS];
nthreads = atoi(argv[1]);
nelems = (1L << atoi(argv[2])); // 2^log_nelems
nelems_per_thread = nelems / nthreads;
// 创建线程
for (i = 0; i < nthreads; i++) {
myid[i] = i;
pthread_create(&tid[i], NULL, sum, &myid[i]);
}
for (i = 0; i < nthreads; i++)
pthread_join(tid[i], NULL);
// 汇总部分和
for (i = 0; i < nthreads; i++)
result += psum[i];
// 验证结果
if (result == (nelems * (nelems - 1) / 2))
printf("Correct Result=%lld\n", result);
else
printf("Error: result=%lld\n", result);
exit(0);
}
```
**并行求和示意图**
```mermaid
graph TD
subgraph "数据划分"
DATA["0, 1, 2, 3, 4, 5, 6, 7, ..."]
DATA --> T0["线程0: 0 ~ N/p-1"]
DATA --> T1["线程1: N/p ~ 2N/p-1"]
DATA --> T2["线程2: 2N/p ~ 3N/p-1"]
DATA --> T3["线程3: 3N/p ~ N-1"]
end
T0 --> S0["psum[0]"]
T1 --> S1["psum[1]"]
T2 --> S2["psum[2]"]
T3 --> S3["psum[3]"]
S0 --> SUM["result = psum[0]+psum[1]+psum[2]+psum[3]"]
S1 --> SUM
S2 --> SUM
S3 --> SUM
style DATA fill:#e1f5fe
style SUM fill:#e8f5e9
```
### 6.2 加速比与效率
**加速比Speedup**S_p = T_1 / T_p
- T_1单线程执行时间
- T_pp 个线程并行执行时间
- 理想加速比 S_p = p线性加速
**效率Efficiency**E_p = S_p / p = T_1 / (p * T_p)
- 理想效率 E_p = 1100%
- 实际效率 < 1因为存在线程创建、同步、通信等开销
```mermaid
graph LR
subgraph "加速比曲线"
X["线程数 p"] --> Y["加速比 Sp"]
IDEAL["理想: Sp=p"] -.-> Y
ACTUAL["实际: Sp<p"] --> 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["复制地址空间<br/>(写时复制)"]
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["主线程<br/>接收任务"] -->|"放入缓冲区"| 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/)