一文搞懂多线程:解锁并发编程

4 阅读39分钟

一文搞懂多线程

引言:多线程

单位早些年在团队里面每个人每周都需要分享一节技术课程,近期在整理技术栈的时候翻到些许久前筹备的ppt,便也在此整理一下多线程的知识点。

在服务端开发领域,我们无可避免会接触到一个核心技术,即多线程。多线程编程是提升程序并发处理能力、最大化利用 CPU 资源的关键手段,更是后端工程师必须吃透的基础能力。

本文将从线程基础概念多线程安全保障多线程适用场景线程池工程实现四个维度,逐层拆解多线程编程的底层原理与落地细节。

线程基础概念:理解进程与线程的本质差异

要理解多线程,必须先理清进程线程的定义边界、资源分配规则与运行机制,这是理解并发执行、线程调度与资源竞争的根本前提。

进程与线程的核心定义

  • 进程:系统中正在运行的应用程序实例,程序启动加载后即成为进程。进程是操作系统资源分配的最小单位,系统会为每个进程分配独立的逻辑内存空间、文件与网络句柄等资源,进程之间相互隔离、数据不互通,具备强独立性。
  • 线程:进程内部独立运行的执行流,是操作系统 CPU 调度与执行的最小单位,也是进程任务的实际执行者。一个进程可包含一至多个线程,所有线程共享进程的全局资源,仅保留自身执行所需的私有数据;程序从 main 函数启动主线程,再通过创建子线程实现并发处理。

简单来说,进程是资源的容器,线程是容器内的执行单元。进程创建与切换开销极大,线程因共享进程资源,创建、销毁与切换成本远低于进程,这也是多线程成为高并发编程首选的核心原因。

进程的核心组成(共享资源池)

进程作为资源容器,其内部的核心资源会被同进程下的所有线程共享,这是多线程并发的基础:

image.png

  • 内存:进程拥有独立的逻辑内存寻址空间,进程间内存完全隔离、互不干扰;进程内部的全局变量、堆内存、静态数据区等内存资源,由所属所有线程共享,是线程间实现数据交互的核心载体。
  • 文件/网络句柄:文件描述符、网络套接字(Socket)、动态链接库等属于系统级全局资源,支持多进程共同访问与资源竞争(如多进程打开同一文件、抢占同一网络端口均为系统允许的操作);同一进程内打开的各类句柄资源,可被其下所有线程直接访问和操作。
  • 线程:是进程实际任务的执行者。我们的主线程入口,main函数会不断的进行函数调用,去完成对应的实际任务。
线程的核心组成(私有资源)

线程拥有独立私有、不与同进程其他线程共享的核心执行结构,这是线程能够独立调度、独立运行的基础保障:

image.png

  • 栈:日常认知中 “堆栈” 的,栈是线程独有的,专门用于存储线程运行状态、局部自动变量及函数调用栈帧。每个线程的栈在创建时独立初始化,线程间栈空间相互隔离;操作系统切换线程时会自动完成栈上下文切换,即切换ESP寄存器。栈空间由系统自动管理,无需高级语言显式分配与释放。
  • 程序计数器(PC):每个线程都有一个私有的程序计数器(PC 计数器),本质是线程专属的指令地址指针,用于记录下一条即将执行的指令地址。多线程并发执行的核心,就是 CPU 在不同线程间快速切换执行,即特定时刻只执行某一个线程的任务,切换时通过 PC 计数器精准记录每个线程的执行断点(包括字节码执行环境中的当前字节码地址),切换后可精准恢复执行位置,保障并发执行的连贯性。
  • 线程本地存储(TLS):线程本地存储是为每个线程单独分配的独立私有内存区域,专门用于存放线程级私有数据。通过 TLS 可实现线程间数据隔离,从根本上避免多线程并发访问引发的数据竞争问题。

注意的是,ESP 是堆栈指针寄存器,它指向栈段内堆栈顶部的精确地址,ESP 会随着函数运行时的变量、函数返回数据的压栈 / 弹栈操作动态变化。。其次,栈是每个线程私有的内存区域,现代操作系统通过虚拟内存分页机制为栈提供了严格的访问保护,默认情况下同进程内其他线程无法直接访问、修改该线程栈区,直接访问会触发内存访问异常;仅可通过操作系统专用 API(如 Windows WriteProcessMemory、Linux ptrace)实现跨线程栈区修改。

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

// 线程局部存储(TLS):每个线程独立副本
__thread int var = 0;
// 全局共享变量:所有线程共用一份
int p_var = 0;
// 循环次数(1000万次)
#define LOOP_TIMES 10000000

// 线程执行函数
void* worker(void* arg) {
    int idx = (int)arg;
    // 循环累加
    for (int i = 0; i < LOOP_TIMES; i++) {
        p_var++;
        var++;
    }
    // 打印结果+内存地址
    printf("thread:%d  var=%d, p_var=%d  [var地址:%p, p_var地址:%p]\n",
           idx, var, p_var, &var, &p_var);
    return NULL;
}

int main() {
    pthread_t pid1, pid2;
    printf("初始p_var = %d\n", p_var);

    // 创建两个线程
    pthread_create(&pid1, NULL, worker, (void*)1);
    pthread_create(&pid2, NULL, worker, (void*)2);

    // 等待线程执行完毕
    pthread_join(pid1, NULL);
    pthread_join(pid2, NULL);

    return 0;
}
进程和线程的资源划分

image.png

这张图清晰呈现了进程与线程的资源划分核心逻辑:

  • 进程维度:每个进程是完全独立的资源容器,拥有专属的动态堆、静态数据与程序代码,进程间资源完全隔离,互不干扰;同一进程内部,堆、静态数据、程序代码为所有线程共享,是多线程并发的资源基础。
  • 线程维度:线程作为进程内的执行单元,仅私有独立的栈与寄存器上下文,所有执行依赖的资源均来自所属进程;多线程共享进程资源,因此线程间通信成本极低,但也由此引入了并发竞争的问题。

线程生命周期与运行机制

线程从创建到销毁的完整生命周期,是理解线程调度、并发控制的核心基础,一个标准线程的生命周期包含 5 种核心状态,各状态间通过系统调用或调度触发流转:

线程的 5 种核心状态
  • 新建态(New) :线程对象已创建,但尚未调用启动方法(如pthread_create),此时线程仅完成了资源初始化,未获得 CPU 调度资格,处于待启动状态。
  • 就绪态(Runnable) :线程已完成启动,具备执行条件,正在等待 CPU 分配时间片。在多核 CPU 环境下,多个就绪态线程可被同时调度执行;单核 CPU 中则通过时间片轮转快速切换,营造 “并发执行” 的效果。
  • 运行态(Running) :线程获得 CPU 时间片,正在执行任务代码。CPU 会为每个线程分配固定的时间片(通常为毫秒级),时间片耗尽后,线程会主动让出 CPU,回到就绪态等待下一次调度。
  • 阻塞态(Blocked) :线程因等待某一条件(如锁、I/O 操作、sleep 休眠、pthread_join等待子线程结束)主动放弃 CPU,暂时不参与调度。当等待条件满足(如锁被释放、I/O 完成、休眠时间到),线程会被唤醒并回到就绪态,与其他就绪线程重新竞争「被 CPU 调度运行」的资格,竞争成功后获得 CPU 时间片,按时间片轮转规则执行。
  • 终止态(Terminated) :线程任务执行完毕、主动退出(如returnpthread_exit)或因异常终止,线程资源进入回收阶段,生命周期结束,无法再次启动。
线程状态流转核心逻辑

线程的状态流转遵循严格的调度规则,核心流转路径如下:

  1. 新建 → 就绪:调用线程启动方法后,线程完成初始化,进入就绪队列等待 CPU 调度。
  2. 就绪 → 运行:CPU 调度选中该线程,分配时间片,线程开始执行任务。
  3. 运行 → 就绪:线程时间片耗尽,或被更高优先级线程抢占,主动让出 CPU,回到就绪队列。
  4. 运行 → 阻塞:线程执行过程中触发等待条件(如加锁失败、I/O 阻塞、sleep),主动放弃 CPU,进入阻塞态。
  5. 阻塞 → 就绪:等待条件满足,线程被唤醒,重新进入就绪队列竞争 CPU。
  6. 运行 → 终止:线程任务执行完毕、主动退出或异常终止,生命周期结束。

image.png

线程调度与并发执行原理

操作系统的线程调度是多线程并发的核心保障,主流调度机制为时间片轮转调度 + 优先级调度

  • 时间片轮转:系统为每个就绪态线程分配固定长度的时间片,CPU 按顺序执行线程,时间片耗尽后切换到下一个线程,循环往复,实现宏观上的并发、微观上的串行执行。
  • 优先级调度:线程可设置不同优先级,高优先级线程会优先获得 CPU 时间片,甚至可抢占低优先级线程的执行权,保障核心任务的响应速度。

需要注意的是,线程调度完全由操作系统内核控制,应用程序仅能通过设置优先级、休眠、加锁等方式间接影响调度,无法直接干预 CPU 的线程切换。

image.png

线程常用核心接口(POSIX 线程库)

在 Linux/Unix 系统中,多线程编程基于 POSIX 线程库(pthread)实现,所有接口均需引入头文件#include <pthread.h>,核心接口与功能如下:

接口描述
int pthread_create(pthread_t *thread, pthread_attr_t *attr, void*(*start_routine)(void*), void *arg);创建一个新线程,线程创建后,调度顺序由操作系统内核决定,无法保证哪个线程会优先获得 CPU 执行权。
int pthread_join(pthread_t th, void **thread_return);以阻塞方式等待指定线程结束,函数返回时,被等待线程的资源会被系统回收。若线程已结束,函数会立即返回;待等待的线程必须为可结合态(joinable)pthread_createpthread_join 是线程创建与资源回收的经典组合。
void pthread_exit(void *retval);终止调用它的线程并返回一个指向某个对象的指针(即void *retval)。注意:严禁返回指向局部变量的指针!线程调用该函数后,其栈上的局部变量会被销毁,指针将成为野指针,引发程序崩溃或数据异常。
int pthread_cancel(pthread_t tid);向指定线程发送取消请求,请求终止同一进程内的其他线程。需注意:线程是否能被立即取消,取决于其取消类型与状态(如是否处于可取消点)。
pthread_t pthread_self (void); 返回当前线程的 线程 ID,用于标识进程内的唯一线程,可用于线程间身份区分与逻辑控制。
int pthread_detach (pthread_t tid);参数是指定线程的ID,将指定ID的线程设置为 分离态(detached) 指定的ID的线程变成分离状态;分离态线程退出时,资源会自动释放,无需其他线程通过 pthread_join 回收;若线程非分离态,则需保留线程 ID 与退出状态,直至其他线程执行 pthread_join
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <pthread.h>
#include <stdint.h> 

// 错误处理宏:打印详细错误信息并退出
#define sys_err(S) do { \
    fprintf(stderr, "[%s:%d] %s : %s\n", __FILE__, __LINE__, S, strerror(errno)); \
    exit(-1); \
} while (0)

void *func(void *arg) {
    // 接收传入的参数:void* → intptr_t → int
    int input_num = (intptr_t)arg;
    printf("线程中接收到的参数:%d\n", input_num);

    // 线程执行完后返回一个自定义值
    int return_num = 100;
    // 返回值转换:int → intptr_t → void*
    return (void *)(intptr_t)return_num;
}

int main() {
    pthread_t tid;
    int arg = 1;                // 传给线程的参数
    void *thread_ret;           // 用于接收线程的返回值(void*类型)

    // 创建线程:用intptr_t安全转换整数→指针
    if (pthread_create(&tid, NULL, func, (void *)(intptr_t)arg) != 0) {
        sys_err("pthread_create fail");
    }

    // 作用:阻塞等待线程结束,并把线程的返回值存入 thread_ret
    if (pthread_join(tid, &thread_ret) != 0) {
        sys_err("pthread_join fail");
    }

    // 解析线程返回值:void* → intptr_t → int
    int ret_num = (intptr_t)thread_ret;
    printf("主线程接收到线程的返回值:%d\n", ret_num);

    return 0;
}

多线程安全保障:并发竞争与保障机制

多线程依托资源共享实现高效并发,在显著提升程序运行效率的同时,也给带来了严峻的线程安全问题。当多个线程同时对同一共享资源进行读写操作时,极易引发数据不一致、运行结果不可预期等异常状况,严重时还会出现数据错乱、死锁,甚至直接导致程序崩溃。

临界资源与临界区

临界资源:指可被多个执行流(进程 / 线程)共享访问的资源,包含共享变量、共享内存、文件句柄、网络连接、硬件设备等(既包含进程内的共享资源,也涵盖跨进程共享资源)。这类资源自身不具备天然的并发互斥保护机制,无法阻止多个执行流同时操作;为保证数据一致性与执行结果正确,在逻辑上必须要求同一时刻仅允许一个执行流对其进行访问,多执行流无序并发操作极易引发数据竞争、执行结果错乱,是并发编程中线程安全问题的核心根源。

临界区:指程序中用于访问、操作临界资源的代码片段。为保障临界资源的数据一致性与线程安全,临界区需遵循互斥访问原则,即同一时刻仅允许一个执行流进入临界区执行,其他试图访问的执行流需阻塞等待,直至当前执行流离开临界区后,方可竞争进入。

线程同步的常用接口

整理 Linux 环境下 POSIX 标准线程同步核心编程接口,涵盖初始化、加解锁、销毁等基础操作,接口返回 0 代表执行成功,非 0 值对应系统异常错误码

线程同步的方式常用接口与说明
互斥锁(Mutex)int pthread_mutex_init(pthread_mutex_t *mutex, const pthread_mutexattr_t *mutexattr); 初始化互斥锁,为锁分配资源并配置属性
int pthread_mutex_lock(pthread_mutex_t *mutex); 以阻塞方式加锁:若锁已被占用,线程会挂起等待,直到成功获取锁
int pthread_mutex_trylock(pthread_mutex_t *mutex); 以非阻塞方式加锁:若锁被占用,直接返回EBUSY错误,不会挂起线程等待
int pthread_mutex_unlock(pthread_mutex_t *mutex); 解锁:释放持有的互斥锁,唤醒等待该锁的线程
int pthread_mutex_destroy(pthread_mutex_t *mutex); 销毁锁:释放互斥锁占用的系统资源
条件变量(Condition Variable)int pthread_cond_init(pthread_cond_t *cond, const pthread_condattr_t *cond_attr); 初始化条件变量,用于线程间的状态同步与等待唤醒
int pthread_cond_wait(pthread_cond_t *cond, pthread_mutex_t *mutex); 无条件等待:阻塞线程直到条件被唤醒,调用时会自动释放关联的互斥锁,被唤醒后重新持有锁
int pthread_cond_signal(pthread_cond_t *cond); 激活条件变量(唤醒单个线程) :唤醒一个正在等待该条件变量的线程
int pthread_cond_destroy(pthread_cond_t *cond); 销毁条件变量:释放条件变量占用的系统资源
读写锁(RWLock)int pthread_rwlock_init(pthread_rwlock_t *restrict rwlock, const pthread_rwlockattr_t *restrict attr); 读写锁初始化,适配读多写少的高并发场景
int pthread_rwlock_rdlock(pthread_rwlock_t *rwlock); 阻塞式读锁:允许多个线程同时持有读锁共享访问,写锁占用时线程阻塞
int pthread_rwlock_wrlock(pthread_rwlock_t *rwlock); 阻塞式写锁:写锁具有独占性,读写、写写互斥,锁被占用时线程阻塞
int pthread_rwlock_unlock(pthread_rwlock_t *rwlock); 解锁:统一释放读锁或写锁
int pthread_rwlock_tryrdlock(pthread_rwlock_t *rwlock); 非阻塞式读锁:尝试加读锁,失败直接返回错误,不阻塞线程
int pthread_rwlock_trywrlock(pthread_rwlock_t *rwlock); 非阻塞式写锁:尝试加写锁,失败直接返回错误,不阻塞线程
int pthread_rwlock_timedrdlock(pthread_rwlock_t *restrict rwlock, const struct timespec *restrict abs_timeout); 定时读锁:限时阻塞加读锁,超时未获取锁则返回
int pthread_rwlock_timedwrlock(pthread_rwlock_t *restrict rwlock, const struct timespec *restrict abs_timeout); 定时写锁:限时阻塞加写锁,超时未获取锁则返回
int pthread_rwlock_destroy(pthread_rwlock_t *rwlock); 销毁读写锁:释放读写锁占用的系统资源
信号量(Semaphore)int sem_init(sem_t *sem, int pshared, unsigned int value); 初始化信号量pshared指定共享范围(0 为线程间共享,1 为进程间共享),value为信号量初始计数
int sem_post(sem_t *sem); 信号量值加 1:释放信号量,唤醒等待该信号量的线程
int sem_wait(sem_t *sem); 信号量值减 1:阻塞获取信号量,计数为 0 时线程挂起等待
int sem_destroy(sem_t *sem); 销毁信号量:释放信号量占用的系统资源

线程同步的应用场景

各类线程同步机制依据不同设计逻辑适配多样化并发场景,需结合共享资源的读写特性、线程 / 进程的协作模式合理选择。其核心目标是:在保障共享资源安全、有序访问的前提下,最大化提升程序的并发执行效率与运行稳定性。

线程同步的方式描述
互斥锁互斥锁是面向资源独占访问优化的同步原语,核心是保证资源占用的唯一性。线程通过加锁锁定临界资源,锁定期间其他线程无法操作受保护数据,以此保证临界区代码执行的原子性与互斥性。
条件变量条件变量是面向线程等待唤醒优化的同步原语,通常需与互斥锁配合使用。它用于线程间无竞争地等待特定条件成立,实现「等待 - 通知」的线程协作模型,避免线程空轮询消耗资源。
信号量信号量是通用性更强的同步原语,既可以实现互斥上锁,也可以实现线程等待唤醒;相较于互斥锁,其使用逻辑更复杂,系统开销也相对更高。信号量适用于多线程、多任务同步场景,其 post/wait 操作无需由同一线程执行,同时支持跨进程同步。
读写锁读写锁提供共享读、独占写两种访问模式:
1. 同一时刻仅允许一个线程持有写锁,允许多个线程同时持有读锁;
2. 若有线程持有读锁,其他线程可继续获取读锁,但无法获取写锁;
3. 若有线程持有写锁,其他线程既无法获取读锁,也无法获取写锁;
4. 读操作与写操作严格互斥,不可并行执行。读写锁适用于读操作远多于写操作的场景,能显著提升并发读取的性能。
#include <stdio.h>      // 标准输入输出头文件
#include <stdlib.h>     // 标准库头文件(exit等函数)
#include <pthread.h>    // 线程库头文件
#include <semaphore.h>  // 信号量头文件
#include <unistd.h>     // Unix标准函数头文件(sleep)
#include <errno.h>      // 错误码头文件
#include <string.h>     // strerror函数头文件

// 全局变量 - 互斥锁相关
pthread_mutex_t mutex;       // 互斥锁对象
int count = 0;               // 共享计数器

// 全局变量 - 条件变量相关(生产者-消费者)
pthread_cond_t cond;         // 条件变量对象
pthread_mutex_t cond_mutex;  // 条件变量关联的互斥锁
int has_data = 0;            // 数据就绪标记(0无/1有)

// 全局变量 - 读写锁相关
pthread_rwlock_t rwlock;     // 读写锁对象
int data = 100;              // 读多写少的共享数据

// 全局变量 - 信号量相关
sem_t sem;                   // 信号量对象

// 错误处理函数:打印错误信息并退出
void sys_err(const char *msg) {
    fprintf(stderr, "[ERROR] %s: %s (errno=%d)\n", msg, strerror(errno), errno);
    exit(EXIT_FAILURE);
}

// 1. 互斥锁示例:保护共享计数器自增
void* mutex_worker(void* arg) {
    // 加互斥锁(阻塞式)
    int ret = pthread_mutex_lock(&mutex);
    if (ret != 0) sys_err("pthread_mutex_lock failed");

    count++;  // 临界区:操作共享变量
    printf("[互斥锁] 线程%lu执行,count = %d\n", (unsigned long)pthread_self(), count);

    // 释放互斥锁
    ret = pthread_mutex_unlock(&mutex);
    if (ret != 0) sys_err("pthread_mutex_unlock failed");

    return NULL;
}

// 2. 条件变量示例 - 生产者:生产数据并唤醒消费者
void* producer(void* arg) {
    // 加锁保护条件变量关联的共享数据
    int ret = pthread_mutex_lock(&cond_mutex);
    if (ret != 0) sys_err("producer: pthread_mutex_lock failed");

    has_data = 1;  // 标记数据已生产
    printf("[条件变量] 生产者线程%lu:已生产数据,has_data = %d\n", (unsigned long)pthread_self(), has_data);

    // 唤醒一个等待的消费者线程
    ret = pthread_cond_signal(&cond);
    if (ret != 0) sys_err("pthread_cond_signal failed");

    // 释放互斥锁
    ret = pthread_mutex_unlock(&cond_mutex);
    if (ret != 0) sys_err("producer: pthread_mutex_unlock failed");

    return NULL;
}

// 2. 条件变量示例 - 消费者:等待数据并消费
void* consumer(void* arg) {
    // 加锁保护条件变量关联的共享数据
    int ret = pthread_mutex_lock(&cond_mutex);
    if (ret != 0) sys_err("consumer: pthread_mutex_lock failed");

    // 循环等待(避免虚假唤醒)
    while (has_data == 0) {
        printf("[条件变量] 消费者线程%lu:无数据,等待中...\n", (unsigned long)pthread_self());
        // 等待条件变量(自动释放锁,唤醒后重新获取)
        ret = pthread_cond_wait(&cond, &cond_mutex);
        if (ret != 0) sys_err("pthread_cond_wait failed");
    }

    has_data = 0;  // 标记数据已消费
    printf("[条件变量] 消费者线程%lu:已消费数据,has_data = %d\n", (unsigned long)pthread_self(), has_data);

    // 释放互斥锁
    ret = pthread_mutex_unlock(&cond_mutex);
    if (ret != 0) sys_err("consumer: pthread_mutex_unlock failed");

    return NULL;
}

// 3. 读写锁示例 - 读线程:并发读取共享数据
void* reader(void* arg) {
    int idx = *(int*)arg;  // 解析线程编号

    // 加读锁(允许多线程并发持有)
    int ret = pthread_rwlock_rdlock(&rwlock);
    if (ret != 0) sys_err("reader: pthread_rwlock_rdlock failed");

    // 读操作(并发执行)
    printf("[读写锁] 读线程%d:data = %d(持有读锁中)\n", idx, data);
    sleep(1);  // 模拟读操作耗时

    // 释放读锁
    ret = pthread_rwlock_unlock(&rwlock);
    if (ret != 0) sys_err("reader: pthread_rwlock_unlock failed");

    printf("[读写锁] 读线程%d:释放读锁\n", idx);
    return NULL;
}

// 3. 读写锁示例 - 写线程:独占修改共享数据
void* writer(void* arg) {
    // 加写锁(独占,阻塞所有读/写操作)
    int ret = pthread_rwlock_wrlock(&rwlock);
    if (ret != 0) sys_err("writer: pthread_rwlock_wrlock failed");

    // 写操作(独占执行)
    data += 10;
    printf("[读写锁] 写线程:修改data = %d(持有写锁中)\n", data);
    sleep(1);  // 模拟写操作耗时

    // 释放写锁
    ret = pthread_rwlock_unlock(&rwlock);
    if (ret != 0) sys_err("writer: pthread_rwlock_unlock failed");

    printf("[读写锁] 写线程:释放写锁\n");
    return NULL;
}

// 4. 信号量示例:资源限流(最多2个线程并发)
void* sem_worker(void* arg) {
    int idx = *(int*)arg;  // 解析线程编号

    // 获取信号量(计数器-1,为0则阻塞)
    int ret = sem_wait(&sem);
    if (ret != 0) sys_err("sem_wait failed");

    // 临界区:限流执行
    printf("[信号量] 线程%d:开始执行(当前并发数-1)\n", idx);
    sleep(2);  // 模拟任务耗时

    // 释放信号量(计数器+1,唤醒等待线程)
    ret = sem_post(&sem);
    if (ret != 0) sys_err("sem_post failed");

    printf("[信号量] 线程%d:执行完成(当前并发数+1)\n", idx);
    return NULL;
}

int main() {
    // 1. 初始化同步工具
    if (pthread_mutex_init(&mutex, NULL) != 0) sys_err("pthread_mutex_init failed");
    if (pthread_cond_init(&cond, NULL) != 0) sys_err("pthread_cond_init failed");
    if (pthread_mutex_init(&cond_mutex, NULL) != 0) sys_err("cond_mutex init failed");
    if (pthread_rwlock_init(&rwlock, NULL) != 0) sys_err("pthread_rwlock_init failed");
    // 信号量初始化:线程间共享(pshared=0),初始计数=2
    if (sem_init(&sem, 0, 2) != 0) sys_err("sem_init failed");

    // 2. 互斥锁测试(2个线程修改共享计数器)
    pthread_t t1, t2;
    pthread_create(&t1, NULL, mutex_worker, NULL);
    pthread_create(&t2, NULL, mutex_worker, NULL);
    pthread_join(t1, NULL);
    pthread_join(t2, NULL);
    printf("----------------------------------------\n");

    // 3. 条件变量测试(先创建消费者,避免信号丢失死锁)
    pthread_t prod, cons;
    pthread_create(&cons, NULL, consumer, NULL);  // 先启动消费者等待
    sleep(1);  // 保证消费者先进入等待状态
    pthread_create(&prod, NULL, producer, NULL);   // 后启动生产者发送信号
    pthread_join(prod, NULL);
    pthread_join(cons, NULL);
    printf("----------------------------------------\n");

    // 4. 读写锁测试(3个读线程+1个写线程)
    pthread_t r1, r2, r3, w1;
    int r_idx1=1, r_idx2=2, r_idx3=3;
    pthread_create(&r1, NULL, reader, &r_idx1);
    pthread_create(&r2, NULL, reader, &r_idx2);
    pthread_create(&r3, NULL, reader, &r_idx3);
    pthread_create(&w1, NULL, writer, NULL);
    pthread_join(r1, NULL);
    pthread_join(r2, NULL);
    pthread_join(r3, NULL);
    pthread_join(w1, NULL);
    printf("----------------------------------------\n");

    // 5. 信号量测试(4个线程,限流2个并发)
    pthread_t s1, s2, s3, s4;
    int s_idx1=1, s_idx2=2, s_idx3=3, s_idx4=4;
    pthread_create(&s1, NULL, sem_worker, &s_idx1);
    pthread_create(&s2, NULL, sem_worker, &s_idx2);
    pthread_create(&s3, NULL, sem_worker, &s_idx3);
    pthread_create(&s4, NULL, sem_worker, &s_idx4);
    pthread_join(s1, NULL);
    pthread_join(s2, NULL);
    pthread_join(s3, NULL);
    pthread_join(s4, NULL);

    // 6. 销毁同步工具,释放资源
    pthread_mutex_destroy(&mutex);
    pthread_cond_destroy(&cond);
    pthread_mutex_destroy(&cond_mutex);
    pthread_rwlock_destroy(&rwlock);
    sem_destroy(&sem);

    return 0;
}

多线程的致命陷阱(死锁)

多线程是一把双刃剑,在提升效率的同时,也带来了死锁的致命风险。死锁指两个或多个线程互相持有对方所需的锁资源,导致所有线程永久阻塞、程序卡死。最好的死锁处理方法,就是在编写程序时提前识别并规避风险。工程中最常见的有如下三类死锁场景:

忘记释放锁(异常分支导致锁永久占用)

这是开发中最易出现的低级死锁问题,核心诱因是加锁与解锁未严格配对,线程加锁后因异常、提前返回等逻辑跳过解锁步骤,导致锁被永久占用

void data_process()
{
    EnterCriticalSection();  // 进入临界区(加锁)
    if(/* error happens */)  // 异常触发,直接返回
        return;              // 未执行解锁,锁被永久占用
    LeaveCriticalSection();  // 正常流程解锁(离开临界区)
}

线程调用EnterCriticalSection()成功加锁后,若在临界区执行过程中触发错误判断、异常抛出、提前return/break等分支,会直接退出当前函数,完全跳过LeaveCriticalSection()解锁步骤。这会导致该临界区锁被当前线程永久持有,其他所有线程申请该锁时,都会陷入无限阻塞状态,最终整个程序模块卡死无响应。其核心诱因是解锁逻辑仅放在正常执行流程中,未覆盖所有异常、提前退出的分支,导致加锁解锁不配对。

重复申请锁(同一线程递归加锁)

这类死锁属于线程自死锁,核心诱因是使用非递归互斥锁时,同一线程在持有锁的前提下,再次对同一把锁执行加锁操作,自己阻塞自己。

void sub_func()
{
    EnterCriticalSection();  // 子函数内加锁
    do_something();
    LeaveCriticalSection();  // 子函数内解锁
}

void data_process()
{
    EnterCriticalSection();  // 外层函数加锁
    sub_func();               // 调用子函数,子函数内再次申请同一锁
    LeaveCriticalSection();  // 外层函数解锁
}

默认创建的临界区锁为非递归锁,不允许同一线程重复持有。线程在外层data_process()中调用EnterCriticalSection()成功加锁后,调用子函数sub_func(),子函数内再次对同一把锁执行EnterCriticalSection()加锁操作,此时线程会因重复申请锁而直接阻塞,无法执行后续的LeaveCriticalSection()解锁操作,形成 “自己等待自己释放锁” 的自死锁,线程永久卡死。

多线程多锁申请(经典循环等待)

这是最经典、最高发、最难排查的死锁场景,核心诱因是多个线程按相反顺序申请多把锁,形成锁等待闭环。

// 线程1:先申请cs1,再申请cs2
void data_process1()
{
    EnterCriticalSection(&cs1);
    EnterCriticalSection(&cs2);
    do_something1();
    LeaveCriticalSection(&cs2);
    LeaveCriticalSection(&cs1);
}

// 线程2:先申请cs2,再申请cs1
void data_process2()
{
    EnterCriticalSection(&cs2);
    EnterCriticalSection(&cs1);
    do_something2();
    LeaveCriticalSection(&cs1);
    LeaveCriticalSection(&cs2);
}

存在两把独立的临界区锁cs1cs2,线程 1 的加锁顺序是先cs1、后cs2,线程 2 的加锁顺序是先cs2、后cs1。在极端调度场景下:

  • 线程 1 执行EnterCriticalSection(&cs1),成功持有cs1锁,随后申请cs2锁;
  • 同时线程 2 执行EnterCriticalSection(&cs2),成功持有cs2锁,随后申请cs1锁;此时线程 1 持有cs1等待cs2,线程 2 持有cs2等待cs1,两个线程互相持有对方所需的锁资源,且均不主动释放自身锁,形成完美的循环等待闭环,最终两个线程永久阻塞,程序彻底卡死。
死锁的必要条件与工程预防

死锁发生需同时满足四个条件,破坏任意一个即可从根源避免死锁:

必要条件核心定义工程预设
互斥条件锁资源具有排他性,同一时刻仅允许一个线程持有,其他线程需阻塞等待。仅对写操作、全局变量修改等必须互斥的核心资源加锁;优先通过原子操作(如std::atomic)、无锁结构替代显式互斥锁,降低锁依赖。
请求与保持条件线程持有已获取的旧锁,同时申请新的锁资源,且申请失败时不释放已持有的旧锁。1. 借助一次性原子化申请所有锁的工具函数(如 C++ std::lock),申请成功后执行业务逻辑,失败则全部释放并重试;2. 申请新锁前,主动释放已持有的所有旧锁,避免锁资源叠加占用。
不可剥夺条件线程已获取的锁资源无法被其他线程强制剥夺,仅能由持有线程主动释放。采用非阻塞加锁接口(如 C++ std::mutex::try_lock、POSIX pthread_mutex_trylock),申请新锁失败时,立即释放已持有的所有锁资源,延时后重试,避免线程无限阻塞。
循环等待条件多个线程形成闭环等待链路(线程 A 等待线程 B 的锁,线程 B 等待线程 A 的锁)。统一全局加锁顺序,所有线程严格按锁地址、资源 ID 等固定规则排序后申请多把锁,从根本上打破循环等待链路。
工程实践的核心规则
  1. 加锁解锁必须配对,覆盖全分支:所有加锁操作需严格匹配解锁逻辑,重点处理异常分支、提前return/break、函数退出等场景,杜绝锁泄漏。推荐使用 RAII 机制(如 C++ 标准库的std::lock_guardstd::unique_lock)自动管理锁生命周期,替代手动解锁,从语法层面规避人为遗漏风险。
  2. 避免锁嵌套,必须嵌套则统一顺序:尽量减少多锁嵌套场景;若业务存在跨资源事务等不可避免的嵌套需求,需强制所有线程遵循全局统一的加锁顺序,严禁线程交叉申请锁资源(如线程 A 按 A→B、线程 B 按 B→A 申请)。
  3. 缩小锁粒度,缩短锁持有时间:仅对操作共享资源的核心临界区加锁,禁止在锁内执行 IO 读写、网络请求、复杂计算等耗时操作。通过全局锁拆分局部锁的 “锁细化” 方式,结合快速释放策略,减少锁竞争时长,降低死锁触发概率。
  4. 递归场景兜底使用递归锁:针对递归调用、同一线程重复加锁的场景,主动选用递归互斥锁,如 POSIX 的PTHREAD_MUTEX_RECURSIVE、C++ 标准库的std::recursive_mutex,支持同一线程重复持有同一把锁,避免自死锁问题。
  5. 高并发服务添加死锁检测:交易系统、缓存集群等核心高并发服务需集成死锁检测机制:POSIX 环境可通过pstackgdb排查线程状态,Windows 环境可借助Process ExplorerWinDbg分析锁占用;业务层监控线程阻塞时长,超出阈值自动报警,实时预警并快速定位死锁风险。

volatile:禁止编译器优化的语法关键字

volatile 是 C/C++ 中用于修饰变量的关键字,其核心作用是禁止编译器对该变量的读写指令进行优化删除或编译期指令重排,要求编译器严格保留每一次对该变量的内存读写操作,不做任何编译级别的省略或调整。它并非为多线程并发安全设计,不能保障多线程环境下的可见性、有序性,也无法替代锁或原子操作实现线程安全。

编译器优化逻辑与 volatile 的核心作用

在默认编译优化下,编译器为提升执行效率,会对代码做三类优化,而 volatile 的存在就是为了阻止这些优化作用于目标变量:

  • 省略冗余读写:若变量读写无实际业务意义(如 a = a),编译器会直接删除该操作;
  • 寄存器缓存优化:将变量频繁读写的结果缓存到 CPU 寄存器中,减少内存访问次数;
  • 指令重排优化:对无数据依赖的指令调整执行顺序,提升 CPU 流水线利用率。

volatile 本质是向编译器传递 “变量易变” 的提示,提示该变量的值可能被当前线程之外的因素(如硬件中断、信号处理函数、内存映射设备)修改,因此编译器必须:

  • 每次读写都直接操作内存,不使用寄存器缓存的副本;
  • 完整保留所有读写指令,不省略任何看似 “无意义” 的操作;
  • 不调整 volatile 变量相关指令的编译顺序。
volatile 的两大核心特性:仅作用于编译期

一个被 volatile 声明的变量,主要具备以下两大特性:

  • 禁止编译器优化删除读写:无论变量读写是否看似 “冗余”(如 a = a),编译器都必须完整保留 “内存读取 → 指令执行 → 内存回写” 的全流程,不跳过任何一步;

  • 禁止编译器指令重排:编译器不能调整 volatile 变量相关指令与其他指令的相对顺序,确保编译生成的指令序列与源码逻辑一致。

volatile 编译优化的直观对比

启用编译器优化 的场景下,普通变量与 volatile 修饰变量的编译结果差异,可直观体现其核心作用:

普通变量代码:

int main (int argc, char *argv[])
{
    int a = 2;
    a = a;
    return 0;
}

编译结果:编译器会直接优化删除 a = a 语句,最终汇编仅保留 a = 2 的赋值与 return 0 核心逻辑,减少不必要的内存访问。

image.png

volatile修饰变量代码:

int main (int argc, char *argv[])
{
    volatile int a = 2;
    a = a;
    return 0;
}

编译结果volatile 关键字强制编译器放弃优化,都会完整保留 a = a 的「内存读取 → 寄存器中转 → 内存回写」全套指令,即便该操作在逻辑上看似冗余,也会严格遵循源码执行,直观体现其 “禁止编译优化” 的核心特性。

image.png

多线程适用场景

多线程并非万能解决方案,需结合业务场景合理选型,才能最大化发挥其并发优势,避免不必要的资源开销。以下是多线程的经典适用场景:

  • 网络服务与 Web 服务:常见的浏览器、Web 服务(现代 Web 框架由中间件封装了线程池与线程调度逻辑,开发者无需手动管理)、游戏服务器等各类专用服务器,通过多线程并行处理海量用户请求,提升服务吞吐量与响应速度。
  • 文件与网络传输:FTP 下载、大文件分片传输、多线程文件读写等场景,通过多线程并行拆分任务,大幅缩短传输与处理耗时。
  • 分布式计算与存储:分布式算力调度、分布式存储系统中,通过多线程并行处理数据分片、节点通信、任务调度,提升系统整体处理效率。
  • 多步骤任务协同处理:针对多阶段、多步骤的复杂任务,可根据各步骤的 IO/CPU 特征拆分任务,由主线程分配给不同特性的线程协作处理,实现流水线式并发执行。
  • 大数据处理与数据迁移:数据库海量数据分析、跨库数据迁移、日志清洗等场景,通过多线程并行处理数据分片,充分利用多核 CPU 资源,大幅提升处理速度。

线程的注意事项

多线程编程需严格遵循工程规范,规避并发陷阱,保障程序稳定运行,核心注意事项如下:

  • 避免无意义的资源竞争:尽量减少多线程对同一临界资源的直接竞争;若竞争不可避免,必须通过互斥锁、原子操作等机制做好临界资源的安全保护。
  • 明确线程分工,避免职责混乱:在系统设计阶段,对多线程进行明确的职责划分,避免同一线程处理多种不同类型的任务,降低代码复杂度与并发风险。
  • 合理设置线程数,最大化 CPU 利用率:根据任务类型匹配最优线程数,CPU 密集型程序:CPU 核数与线程数比例建议为 N : N+1(少量冗余线程应对偶发阻塞);IO 密集型程序:CPU 核数与线程数比例建议为 N : 2N+1,计算公式为:最佳线程数 = CPU核数 / (1 - 阻塞系数)(阻塞系数 = 阻塞时间 /(阻塞时间 + 计算时间),IO 密集型程序阻塞系数高,需配置更多线程)。
  • 使用线程池,降低上下文切换开销:通过线程池复用线程,避免频繁创建销毁线程带来的 CPU 上下文切换开销,提升系统稳定性与资源利用率。
  • 高并发场景优化锁性能:高并发环境下,同步调用需重点考量锁的性能损耗:优先选用无锁数据结构;必须加锁时,尽量锁代码块而非整个方法,用对象锁替代类锁,缩小锁粒度、缩短锁持有时间。
  • 统一加锁顺序,规避死锁风险:对多个资源、数据库表、对象同时加锁时,所有线程必须保持一致的加锁顺序,从根源上避免循环等待导致的死锁。
  • 使用同步机制保障共享变量可见性:针对多线程共享变量,C/C++ 中不可使用 volatile 解决内存可见性问题,需通过互斥锁、C11 _Atomic/C++11 std::atomic 原子变量、内存屏障等标准同步机制,实现多线程间的数据一致性与内存可见性。

线程池工程实现:高效多线程管理方案

线程池 (thread pool) 技术是指预先创建并复用固定线程,避免频繁地为了某一项工作而创建和销毁线程;因为系统在创建和销毁线程时所耗费的 CPU 资源很大,如果工作数量多、触发频率很高,每次都为单一一项工作创建线程再销毁线程,这种运行方式的效率是相当低下的,线程池技术正是为解决这样的应用场景而应运而生的。

线程池技术的工作原理:在初始化时就创建一定数量的工作线程,同时为其分配一个工作队列用于存放待处理的工作。当工作队列为空时,表示暂无待处理工作,此时所有线程挂起等待新的工作到来。当新的工作到来并加入队列后,空闲线程会竞争获取并执行该工作,而非按固定顺序依次执行;当其中某个线程处理完手头工作后,该线程会立即重新从工作队列中获取新工作并执行,通过线程复用来提升系统的并行处理效率。

线程池主要接口

  • thread_pool_create:创建线程池
  • thread_pool_add_work:任务分派
  • thread_pool_keep_alive:线程池活跃监测
  • thread_pool_destroy:销毁线程池

线程池的代码示例

#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <string.h>
#include <errno.h>
#include <unistd.h>

// 任务结构体
typedef struct work {
    void *(*process)(void *arg);  // 任务处理函数
    void *arg;                    // 任务参数
    struct work *next;            // 下一个任务
} Work_t;

// 线程池结构体
typedef struct pool {
    pthread_mutex_t queue_lock;   // 队列互斥锁
    pthread_cond_t  queue_ready;  // 任务就绪条件
    Work_t         *work_head;    // 任务队列头
    pthread_t      *work_tid;     // 线程ID数组
    int            pool_destroy;  // 销毁标记 0-运行 1-销毁
    int            queue_cur_size;// 当前任务数
    int            queue_allow_size;// 最大线程数
    int            queue_request_size;// 最大请求数
} Pool_t;

// 创建分离线程
int thread_create_detach(Pool_t *pool, int id) {
    int ret = 0;
    pthread_attr_t attr;

    if (NULL == pool) {
        fprintf(stderr, "point type has null value.\n");
        return -1;
    }

    ret = pthread_attr_init(&attr);
    if (0 != ret) {
        fprintf(stderr, "fail to call pthread_attr_init.\n");
        return -1;
    }

    ret = pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED);
    if (0 != ret) {
        fprintf(stderr, "fail to call pthread_attr_setdetachstate.\n");
        pthread_attr_destroy(&attr);
        return -1;
    }

    ret = pthread_create(&pool->work_tid[id], &attr, routine, (void *)pool);
    if (0 != ret) {
        fprintf(stderr, "fail to call pthread_create.\n");
        pthread_attr_destroy(&attr);
        return -1;
    }

    pthread_attr_destroy(&attr);
    return 0;
}

// 工作线程
void *routine(void *arg) {
    Work_t *work = NULL;
    Pool_t *pool = (Pool_t *)arg;

    while (1) {
        // 加锁
        pthread_mutex_lock(&pool->queue_lock);

        // 等待任务
        while (0 == pool->pool_destroy && 0 == pool->queue_cur_size) {
            pthread_cond_wait(&pool->queue_ready, &pool->queue_lock);
        }

        // 销毁退出
        if (1 == pool->pool_destroy) {
            pthread_mutex_unlock(&pool->queue_lock);
            pthread_exit(NULL);
        }

        // 取任务
        pool->queue_cur_size--;
        work = pool->work_head;
        pool->work_head = work->next;

        // 解锁
        pthread_mutex_unlock(&pool->queue_lock);

        // 执行任务
        if (NULL != work && NULL != work->process) {
            work->process(work->arg);
        }
        free(work);
        work = NULL;
    }
    return NULL;
}

// 创建线程池
int thread_pool_create(Pool_t **pool, int pool_size) {
    int i = 0, ret = 0;
    Pool_t *newpool = NULL;

    if (NULL == pool) {
        fprintf(stderr, "point type has null value.\n");
        return -1;
    }

    newpool = (Pool_t *)calloc(1, sizeof(Pool_t));
    if (NULL == newpool) {
        fprintf(stderr, "fail to call calloc\n");
        return -1;
    }

    // 初始化锁
    ret = pthread_mutex_init(&newpool->queue_lock, NULL);
    if (0 != ret) {
        fprintf(stderr, "fail to call pthread_mutex_init\n");
        free(newpool);
        return -1;
    }

    // 初始化条件变量
    ret = pthread_cond_init(&newpool->queue_ready, NULL);
    if (0 != ret) {
        fprintf(stderr, "fail to call pthread_cond_init\n");
        pthread_mutex_destroy(&newpool->queue_lock);
        free(newpool);
        return -1;
    }

    // 初始化参数
    newpool->work_head = NULL;
    newpool->pool_destroy = 0;
    newpool->queue_cur_size = 0;
    newpool->queue_allow_size = 0;
    newpool->queue_request_size = 0;

    // 申请线程ID内存
    newpool->work_tid = (pthread_t *)calloc(pool_size, sizeof(pthread_t));
    if (NULL == newpool->work_tid) {
        fprintf(stderr, "fail to call calloc\n");
        free(newpool);
        return -1;
    }

    // 创建线程
    for (i = 0; i < pool_size; i++) {
        ret = thread_create_detach(newpool, i);
        if (-1 == ret) {
            fprintf(stderr, "fail to call thread_create_detach.\n");
            newpool->pool_destroy = 1;
            pthread_cond_broadcast(&newpool->queue_ready);
            free(newpool->work_tid);
            pthread_mutex_destroy(&newpool->queue_lock);
            pthread_cond_destroy(&newpool->queue_ready);
            free(newpool);
            return -1;
        }
        newpool->queue_allow_size++;
    }

    *pool = newpool;
    return 0;
}

// 添加任务
int thread_pool_add_work(Pool_t *pool, void *(*process)(void *arg), void *arg) {
    if (NULL == pool || NULL == process || 1 == pool->pool_destroy) {
        return -1;
    }

    Work_t *new_work = (Work_t *)malloc(sizeof(Work_t));
    if (NULL == new_work) return -1;
    new_work->process = process;
    new_work->arg = arg;
    new_work->next = NULL;

    // 加锁入队
    pthread_mutex_lock(&pool->queue_lock);
    Work_t *cur = pool->work_head;
    if (NULL == cur) {
        pool->work_head = new_work;
    } else {
        while (NULL != cur->next) cur = cur->next;
        cur->next = new_work;
    }
    pool->queue_cur_size++;
    pthread_mutex_unlock(&pool->queue_lock);

    // 唤醒线程
    pthread_cond_signal(&pool->queue_ready);
    return 0;
}

// 保活检测
int thread_pool_keep_alive(Pool_t *pool) {
    int ret = 0, idx = 0;

    if (NULL == pool) {
        fprintf(stderr, "fail to call thread_pool_keep_alive, point type has null value.\n");
        return -1;
    }

    if (1 == pool->pool_destroy) {
        fprintf(stderr, "fail to call thread_pool_keep_alive, thread_pool is destroyed.\n");
        return -1;
    }

    // 检测线程存活
    for (idx = 0; idx < pool->queue_allow_size; idx++) {
        ret = pthread_kill(pool->work_tid[idx], 0);
        if (ESRCH == ret) {
            ret = thread_create_detach(pool, idx);
            if (-1 == ret) {
                fprintf(stderr, "fail to call thread_create_detach.\n");
            }
        }
    }
    return 0;
}

// 销毁线程池
int thread_pool_destroy(Pool_t **pool) {
    if (NULL == pool || NULL == *pool) return -1;

    Pool_t *p = *pool;
    p->pool_destroy = 1;
    pthread_cond_broadcast(&p->queue_ready);

    // 释放任务队列
    pthread_mutex_lock(&p->queue_lock);
    Work_t *cur = p->work_head;
    while (NULL != cur) {
        Work_t *tmp = cur;
        cur = cur->next;
        free(tmp);
    }
    pthread_mutex_unlock(&p->queue_lock);

    // 释放资源
    free(p->work_tid);
    pthread_mutex_destroy(&p->queue_lock);
    pthread_cond_destroy(&p->queue_ready);
    free(p);
    *pool = NULL;
    return 0;
}

// 测试任务
void *test_task(void *arg) {
    int num = *(int *)arg;
    printf("线程[%lu] 执行任务:%d\n", pthread_self(), num);
    sleep(1);
    return NULL;
}

// 主函数
int main() {
    Pool_t *pool = NULL;

    thread_pool_create(&pool, 3);
    printf("===== 线程池创建成功 =====\n");

    int nums[5] = {1,2,3,4,5};
    for (int i = 0; i < 5; i++) {
        thread_pool_add_work(pool, test_task, &nums[i]);
    }

    sleep(2);
    thread_pool_keep_alive(pool);
    sleep(5);

    thread_pool_destroy(&pool);
    printf("===== 线程池销毁成功 =====\n");

    return 0;
}