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

24 KiB
Raw Permalink Blame History

第07讲多线程编程

本节目标:理解线程的概念与优势,掌握 POSIX 线程 APIpthread理解竞态条件的成因与解决方案互斥锁、信号量能够实现并行计算和生产者-消费者模型。

前置知识


一、线程概念

1.1 什么是线程

线程Thread 是进程内的一个执行单元。一个进程可以包含多个线程,这些线程共享进程的地址空间,但每个线程拥有自己独立的栈和寄存器状态。

生活比喻

  • 进程 = 一家公司(独立的办公室、财务系统、人员编制)
  • 线程 = 公司里的员工(共享办公室和财务系统,但各有各的工作任务)

1.2 进程 vs 线程

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 详解

int pthread_create(
    pthread_t *tid,              // 输出参数:新线程的 ID
    const pthread_attr_t *attr,  // 线程属性(通常传 NULL
    void *(*start_routine)(void*), // 线程函数(新线程从这里开始执行)
    void *arg                    // 传递给线程函数的参数
);
// 返回值:成功返回 0失败返回错误码
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

#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

#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 竞态条件的时序分析

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

#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-- 不是原子操作?

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保证同一时刻只有一个线程可以进入临界区(访问共享资源的代码段)。其他试图获取已锁定的互斥锁的线程会被阻塞,直到锁被释放。

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() 销毁互斥锁

初始化方式

// 方式1静态初始化
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;

// 方式2动态初始化
pthread_mutex_t mutex;
pthread_mutex_init(&mutex, NULL);

4.3 互斥锁修复竞态

参考 实例源代码/chap6/mutex1.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,通过为每个线程分配独立的内存来避免共享:

// 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如果有等待者则唤醒
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 生产者-消费者问题

生产者-消费者是经典的同步问题:生产者向缓冲区放入数据,消费者从缓冲区取出数据。需要解决:

  • 缓冲区满时生产者必须等待
  • 缓冲区空时消费者必须等待
  • 同一时刻只有一个线程访问缓冲区

使用信号量解决

#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);
    }
}

生产者-消费者流程图

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 的竞态条件:

#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,将一个大数组的求和任务分配给多个线程并行计算:

#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);
}

并行求和示意图

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 加速比与效率

加速比SpeedupS_p = T_1 / T_p

  • T_1单线程执行时间
  • T_pp 个线程并行执行时间
  • 理想加速比 S_p = p线性加速

效率EfficiencyE_p = S_p / p = T_1 / (p * T_p)

  • 理想效率 E_p = 1100%
  • 实际效率 < 1因为存在线程创建、同步、通信等开销
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() 只需要分配一个新的栈和少量元数据。

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() 的创建时间:

// 测量 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

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)
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(使用互斥锁修复):

// 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)