在 Linux 系统编程中,多线程是实现并发任务的核心手段。相比多进程,多线程拥有更低的创建开销、更高的并发效率,是后端开发、嵌入式开发的必备知识点。本文系统梳理 Linux 多线程从基础概念到核心 API、互斥同步机制,再到死锁原理的完整知识体系,适合入门学习与复习回顾。


一、线程基础概论

1. 什么是线程

Linux 中线程是轻量级进程(LWP),线程必须隶属于某一个进程,不能独立存在。

  • 进程:操作系统最小的资源分配单位,拥有独立的地址空间、文件描述符等完整资源。
  • 线程:操作系统最小的执行/调度单位,是进程内部的一条执行流。
  • 一个进程内默认存在一条线程,即主线程;同一进程内的多个线程是平级关系。

2. 线程的作用

实现并发执行,适合处理相对耗时的任务(如 IO 操作、复杂计算),提升程序执行效率与响应速度。


二、进程 vs 线程:核心区别对比

对比维度进程线程
资源关系进程间资源相互独立,不共享地址空间同一进程内的线程共享全局资源(全局变量、堆、文件描述符等),仅栈区独立(默认 8MB)
稳定性稳定性更高,单个进程崩溃不影响其他进程稳定性弱,单个线程崩溃会导致整个进程退出
创建开销开销大,创建新进程需分配独立地址空间(约 3GB)开销小,仅需开辟独立栈区(默认 8MB)
并发度并发度较低,进程切换开销大并发度更高,线程切换开销小

三、POSIX 线程编程基础

Linux 提供 POSIX 线程库(pthread),编程通用步骤:创建线程 → 线程执行任务 → 回收线程资源。

编译时必须链接线程库:

gcc xxx.c -o xxx -lpthread

1. 查看线程信息的命令

ps -eLo pid,ppid,lwp,stat,comm
ps -eLf

其中 LWP 即为轻量级进程(线程)ID。

2. 核心线程 API

(1)创建线程:pthread_create
int pthread_create(pthread_t *thread, const pthread_attr_t *attr,
                   void *(*start_routine) (void *), void *arg);
  • 功能:创建一个新线程
  • 参数:
    • thread:输出参数,返回创建成功的线程 ID
    • attr:线程属性,一般传 NULL 使用默认属性
    • start_routine:线程执行函数(回调函数),函数原型必须为 void* 函数名(void* arg)
    • arg:传递给线程执行函数的参数
  • 返回值:成功返回 0,失败返回错误码
(2)获取当前线程 ID:pthread_self
pthread_t pthread_self(void);
  • 功能:获取当前线程的线程 ID
  • 返回值:返回当前线程 ID,类型为 unsigned long int,格式化输出用 %lu
(3)线程主动退出:pthread_exit
void pthread_exit(void *retval);
  • 功能:子线程主动退出,可携带退出状态
  • 参数:retval:线程退出的返回值,可被 pthread_join 获取
  • 注意:不要用 exit 退出线程,exit 会导致整个进程退出;线程内 return 也可退出线程,但仅能退出当前函数层级。
(4)取消其他线程:pthread_cancel
int pthread_cancel(pthread_t thread);
  • 功能:向指定线程发送取消请求,请求其终止执行
  • 参数:thread:目标线程 ID
  • 返回值:成功返回 0,失败返回非 0 错误码
  • 注意:该函数仅发送请求,不等待线程真正结束;线程是否响应取消取决于自身的取消属性。
(5)阻塞回收线程:pthread_join
int pthread_join(pthread_t thread, void **retval);
  • 功能:阻塞等待指定线程结束,并回收其资源,获取线程退出状态
  • 参数:
    • thread:要回收的线程 ID
    • retval:二级指针,用于接收线程退出的返回值(pthread_exit 的参数)
  • 返回值:成功返回 0,失败返回非 0 错误码
  • 特点:阻塞式调用,目标线程未结束时调用线程会一直等待。
(6)设置线程分离属性:pthread_detach
int pthread_detach(pthread_t thread);
  • 功能:给指定线程设置分离属性;设置后线程退出时,系统会自动回收其栈区等资源,无需主线程调用 pthread_join 回收。
  • 参数:thread:目标线程 ID
  • 返回值:成功返回 0,失败返回非 0 错误码
  • 注意:分离后的线程无法再被 join 回收。

3. 完整示例:多线程创建与回收

下面示例演示主线程创建两个子线程,子线程通过 pthread_self 打印自身线程 ID,并使用 pthread_exit 返回退出信息;主线程通过 pthread_join 阻塞回收子线程并读取返回值。

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

void *thread_func(void *arg)
{
    int num = *(int *)arg;

    printf("子线程 %d 启动,线程 ID:%lu\n",
           num, (unsigned long)pthread_self());

    char *msg = malloc(64);
    if (msg != NULL) {
        snprintf(msg, 64, "线程 %d 正常退出", num);
    }

    pthread_exit((void *)msg);
    return NULL; /* 不会执行到 */
}

int main(void)
{
    pthread_t tid1, tid2;
    int arg1 = 1;
    int arg2 = 2;

    if (pthread_create(&tid1, NULL, thread_func, &arg1) != 0) {
        perror("pthread_create 1");
        return 1;
    }

    if (pthread_create(&tid2, NULL, thread_func, &arg2) != 0) {
        perror("pthread_create 2");
        return 1;
    }

    printf("主线程 ID:%lu\n", (unsigned long)pthread_self());

    void *ret1 = NULL;
    void *ret2 = NULL;

    if (pthread_join(tid1, &ret1) != 0) {
        perror("pthread_join 1");
    }

    if (pthread_join(tid2, &ret2) != 0) {
        perror("pthread_join 2");
    }

    printf("回收结果:%s\n", (char *)ret1);
    printf("回收结果:%s\n", (char *)ret2);

    free(ret1);
    free(ret2);

    return 0;
}

编译并运行:

gcc pthread_demo.c -o pthread_demo -lpthread
./pthread_demo

预期输出示例(子线程执行顺序不固定,线程 ID 数值因环境而异):

主线程 ID:140482621056768
子线程 1 启动,线程 ID:140482612664064
子线程 2 启动,线程 ID:140482604271360
回收结果:线程 1 正常退出
回收结果:线程 2 正常退出

四、线程控制:互斥锁(Mutex)

1. 核心概念

  • 临界资源:多线程环境中会被多个线程读写的共享资源,如全局变量、文件、设备。
  • 互斥:对临界资源的排他性访问,同一时刻仅允许一个线程访问临界资源。
  • 为什么需要互斥:
    例如 i++ 操作在汇编层面至少分为 3 步(读取内存到寄存器、寄存器自增、写回内存)。线程调度可能在任意一步发生切换,若两个线程同时对同一个变量自增,会出现数据一致性错误。
  • 原子操作:一次调度内必须完整执行完毕、不可被打断的代码段;互斥锁可以将临界区代码变为原子操作。

2. 互斥锁使用步骤

定义互斥锁 → 初始化锁 → 访问临界资源前加锁 → 访问结束解锁 → 程序结束销毁锁

(1)定义互斥锁
pthread_mutex_t mutex;
(2)初始化互斥锁
int pthread_mutex_init(pthread_mutex_t *mutex, const pthread_mutexattr_t *attr);
  • 参数:mutex:互斥锁地址;attr:锁属性,一般传 NULL 用默认属性
  • 返回值:成功返回 0,失败返回非 0
(3)加锁
int pthread_mutex_lock(pthread_mutex_t *mutex);
  • 功能:对临界区加锁,加锁后其他线程尝试加锁会阻塞
  • 注意:加锁到解锁之间的代码为临界区,应尽量短小,不要包含休眠、大耗时操作。
(4)解锁
int pthread_mutex_unlock(pthread_mutex_t *mutex);
  • 功能:释放互斥锁,其他阻塞的线程可以竞争获取锁
(5)销毁互斥锁
int pthread_mutex_destroy(pthread_mutex_t *mutex);
  • 功能:使用完毕后销毁互斥锁,释放资源

五、线程控制:同步与信号量

1. 核心概念

  • 同步:在互斥的基础上,让多个线程按照指定的先后顺序访问临界资源,是互斥的一种特例。
  • 互斥 vs 同步:
    • 互斥锁:加锁和解锁在同一个线程内完成,保证资源排他访问。
    • 信号量(同步):PV 操作由不同线程交叉执行(线程 A 释放、线程 B 申请),保证执行顺序。

2. 信号量基础

Linux 提供无名信号量用于线程间同步,常用二值信号量(值只能为 0 或 1):

  • 值为 0:红灯,线程调用申请操作会阻塞
  • 值为 1:绿灯,线程可申请资源,申请后值自动减 1

计数信号量初始值可大于 1,适用于多份相同资源的并发访问场景。

3. 信号量使用步骤

定义信号量 → 初始化信号量 → PV 操作(申请/释放) → 销毁信号量

(1)定义信号量
sem_t sem;
(2)初始化信号量
int sem_init(sem_t *sem, int pshared, unsigned int value);
  • 参数:
    • sem:信号量地址
    • pshared:0 表示线程间使用;非 0 表示进程间使用
    • value:信号量初始值
  • 返回值:成功返回 0,失败返回 -1
(3)P 操作(申请资源):sem_wait
int sem_wait(sem_t *sem);
  • 功能:申请资源。若信号量值 > 0,则值减 1,程序继续;若值为 0,则线程阻塞,直到有资源释放。
  • 返回值:成功返回 0,失败返回 -1
(4)V 操作(释放资源):sem_post
int sem_post(sem_t *sem);
  • 功能:释放资源,信号量值自动加 1,不会阻塞调用线程
  • 返回值:成功返回 0,失败返回 -1
(5)销毁信号量
int sem_destroy(sem_t *sem);
  • 功能:销毁信号量,释放资源

补充:信号量保护的临界区内可包含短耗时、轻度休眠操作;但互斥锁临界区必须尽量精简,禁止长时间休眠。


六、死锁

1. 什么是死锁

由于锁资源申请与释放逻辑不合理,导致多个线程/进程相互等待对方持有的资源,从而都无法继续向前执行的现象。

2. 死锁产生的四个必要条件

  1. 互斥条件:一个资源同一时间只能被一个执行流占用。
  2. 请求与保持条件:执行流因申请新资源阻塞时,不释放已经持有的资源。
  3. 不剥夺条件:执行流已获得的资源,在使用完毕前不能被强行剥夺,只能主动释放。
  4. 循环等待条件:多个执行流之间形成头尾相接的循环等待资源的链路。

四个条件同时满足才会产生死锁;预防死锁只需破坏其中任意一个条件即可。


七、总结

Linux 多线程编程的核心脉络是:

  1. 理解线程本质是轻量级进程,共享进程资源、仅栈独立;
  2. 掌握 pthread 库的创建、退出、回收、分离核心 API;
  3. 掌握互斥锁保护临界资源,理解加锁、解锁、销毁的完整流程;
  4. 掌握信号量实现线程同步,理解 PV 操作与二值信号量;
  5. 理解死锁产生的四个必要条件,并能通过破坏任一条件来预防死锁。
Logo

openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构

更多推荐