进阶 #os#thread#concurrency

线程(Thread)与多线程

线程(Thread)是进程内的"轻量级执行流"——同一进程的多个线程共享内存空间,可以并行处理不同任务,就像厨房里多个厨师共用同一套厨具做不同的菜

为什么要线程?

假设你在写一个文本编辑器:

  • 用户输入文字
  • 同时自动保存
  • 同时检查拼写

如果按”进程”的方式:开三个进程,各自干一件事——但共享同一个文档内容就麻烦了(要用进程间通信)。

线程(Thread) 就是同一个进程内的多个”执行流”——它们共享内存空间,但各有各的执行栈和寄存器。

🏫 类比:厨房团队

  • 进程 = 整家餐厅(有自己的厨房、仓库、菜单)
  • 线程 = 餐厅里的厨师
    • 所有厨师(线程)共用同一套厨具和食材(共享内存)
    • 但每个厨师有自己的围裙和刀(私有的栈和寄存器)
    • 厨师之间可以随时沟通(共享变量),比两个餐厅之间通信容易得多

进程 vs 线程

进程 A                   进程 B
┌──────────────────┐    ┌──────────────────┐
│ 代码段            │    │ 代码段            │
│ 数据段            │    │ 数据段            │
│ 堆                │    │ 堆                │
│ ┌──── 线程 1 ──┐ │    │ ┌──── 线程 1 ──┐ │
│ │ 栈 + 寄存器   │ │    │ │ 栈 + 寄存器   │ │
│ └──────────────┘ │    │ └──────────────┘ │
│ ┌──── 线程 2 ──┐ │    │                   │
│ │ 栈 + 寄存器   │ │    │                   │
│ └──────────────┘ │    │                   │
└──────────────────┘    └──────────────────┘

进程之间:隔离(不共享内存)
线程之间:共享代码段、数据段、堆
          私有栈和寄存器
对比进程线程
地址空间独立共享(同一进程内)
创建开销高(复制地址空间)低(只需分配栈)
切换开销高(TLB 刷新)低(不需刷新页表)
通信方式IPC(管道、消息队列等)直接读共享变量
隔离性强(一个崩了不影响其他)弱(一个线程崩了整个进程挂)
资源占用

💡 进程是资源分配的最小单位,线程是CPU 调度的最小单位。

线程的实现模型

1. 用户级线程(User-Level Thread, ULT)

线程管理完全在用户空间实现,内核不知道线程的存在。

用户空间         线程库(管理多个线程)
                ┌──线程1──线程2──线程3──┐
内核空间          单一线程(核心理不到)

优点:切换极快(不需要系统调用),可以在任何 OS 上实现 缺点:一个线程阻塞会导致所有线程阻塞(内核只看到 1 个进程)

2. 内核级线程(Kernel-Level Thread, KLT)

线程的创建、调度、管理全由内核完成。

用户空间         线程1    线程2    线程3
内核空间         ║       ║       ║
              内核线程1 内核线程2 内核线程3

优点:一个线程阻塞不影响其他线程,可以充分利用多核 缺点:切换需要系统调用(开销大)

3. 混合模型(Linux 的实现)

Linux 用 clone() 系统调用创建线程——它的灵活之处在于可以精细控制共享什么:

// fork()——完全不共享(创建进程)
clone(CLONE_VM | CLONE_VM | 0);

// pthread_create()——共享内存空间(创建线程)
clone(CLONE_VM | CLONE_FS | CLONE_FILES | CLONE_SIGHAND, ...);

💡 在 Linux 中,进程和线程本质上是同一回事——都是 task_struct,区别只在于 clone() 时传的参数不同(是否共享地址空间)。

多线程编程示例

#include <stdio.h>
#include <pthread.h>

#define NUM 5

void* print_message(void* arg) {
    int id = *(int*)arg;
    printf("线程 %d:我在运行!\n", id);
    return NULL;
}

int main() {
    pthread_t threads[NUM];
    int ids[NUM];
    
    // 创建 NUM 个线程
    for (int i = 0; i < NUM; i++) {
        ids[i] = i;
        pthread_create(&threads[i], NULL, print_message, &ids[i]);
        printf("主线程:创建了线程 %d\n", i);
    }
    
    // 等待所有线程完成
    for (int i = 0; i < NUM; i++) {
        pthread_join(threads[i], NULL);
        printf("主线程:线程 %d 已结束\n", i);
    }
    
    return 0;
}

输出结果(每次运行可能不同——取决于调度顺序):

主线程:创建了线程 0
线程 0:我在运行!
主线程:创建了线程 1
线程 1:我在运行!
线程 2:我在运行!
主线程:创建了线程 2
主线程:创建了线程 3
线程 3:我在运行!
主线程:创建了线程 4
线程 4:我在运行!
主线程:线程 0 已结束
...

⚠️ 多个线程的输出交织在一起——这就是并发!如果线程们同时往一个文件写数据,就需要同步机制(竞争条件与临界区)。

线程的优势

1. 利用多核 CPU

在一个 4 核 CPU 上,4 个线程可以真正并行执行(每个核跑一个)——4 个进程也可以,但线程的切换开销更小。

2. 结构清晰

// 不用线程——一个程序里乱跳
void web_server() {
    while (1) {
        accept_connection();      // 等连接
        handle_request();         // 处理请求(如果慢就卡住)
    }
}

// 用线程——每个请求一个线程
void handle_request(int fd) { ... }

void web_server() {
    while (1) {
        int fd = accept();
        pthread_create(&t, NULL, handle_request, &fd);
        // 主线程立即回去等下一个连接
    }
}

3. I/O 不阻塞

一个线程等网络数据时,其他线程可以继续执行——这在 web 服务器中至关重要。

线程的挑战

问题说明后果
竞争条件多个线程同时读写共享变量数据不一致
死锁线程互相等待对方释放资源程序卡死
调试困难线程执行顺序不确定Bug 难以复现

📖 这些并发问题将在后续三个节点中详细讨论:竞争条件与临界区互斥锁与信号量死锁

小结

概念要点
线程定义进程内的轻量级执行流,共享地址空间
vs 进程进程隔离、线程共享;进程切换开销大
用户级线程管理在用户态,切换快但阻塞会影响所有
内核级线程管理在内核,独立阻塞但切换开销大
Linux 实现clone() 统一创建,参数控制共享程度
多线程价值利用多核、结构清晰、I/O 不阻塞

为什么先学这个? 线程是并发的”最小单位”。有了线程,才有了竞争条件与临界区的问题——下一节看看为什么多线程访问共享变量会出问题。