本文基于 Linux C 应用开发学习笔记,对比进程,讲解线程原理、API、线程同步互斥、死锁问题。

一、什么是线程

线程是轻量级进程,用来实现程序多任务并发。

  • ✅ 进程:操作系统资源分配最小单位

  • ✅ 线程:CPU 任务调度最小单位

线程一定隶属于某个进程,一个进程内部可以创建多个线程。

进程与线程内存模型

进程创建的时候分配完整 0‑4G 虚拟地址空间; 同一个进程内所有线程共享进程大部分资源,仅拥有自己独立栈区(默认 8M):

  1. 共享:堆区、.data段、.bss段、字符串常量区、代码文本区、内核空间

  2. 私有:每个线程独立栈区,保存局部变量、函数调用现场。

优势:线程创建、切换开销远小于进程;缺点:共享资源带来竞争风险。

进程 vs 线程对比表

对比项

进程

线程

核心定位

资源分配最小单位

CPU 调度最小单位

创建开销

大,完整拷贝虚拟内存

小,只分配线程栈

内存占用

每个进程独立完整内存空间

同进程线程共享绝大部分内存,仅栈独立

安全性

进程之间完全隔离,一个崩溃不影响其他进程

同进程一个线程崩溃,整个进程全部崩溃

通信方式

必须 IPC(管道、共享内存、信号等)

直接读写全局变量、全局队列即可通信

切换速度

慢

快

二、线程基础 API

⚠️注意:编译线程代码必须链接线程库,编译选项加上 -pthread

1. 创建线程 pthread_create

int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine) (void *), void *arg);

参数说明

  1. thread:输出参数,保存线程 ID

  2. attr:线程属性,NULL使用默认属性

  3. start_routine:线程执行函数,函数格式必须 void* func(void*)

  4. arg:传递给线程函数的参数

返回值:成功返回 0;失败返回非 0 错误码。

2. 线程退出

线程退出有三种方式:

  1. 线程任务函数return返回;

  2. 调用pthread_exit(void *retval)主动退出线程;

  3. 整个进程退出,进程内所有线程全部销毁。

⚠️重要:主线程不要提前 return/exit,否则所有子线程直接被销毁。

3. 线程回收

线程分两种属性:非分离属性(默认)、分离属性

① 非分离属性(默认)

线程结束后资源不会自动释放,必须其他线程调用pthread_join回收,否则资源泄漏。

int pthread_join(pthread_t thread, void **retval);
  • thread:要回收的线程 ID

  • retval:接收线程退出返回值,不需要填NULL

  • 特点:阻塞等待子线程结束。

② 分离属性 detach

调用pthread_detach(tid)设置线程为分离属性。

  • 设置之后:线程退出,操作系统自动回收资源,不需要调用pthread_join。

  • 使用场景:主线程没有机会去 join 子线程。

int pthread_detach(pthread_t tid);

三、线程间通信、临界资源问题

同进程线程共享全局变量、堆内存,因此可以直接读写实现通信。

多线程同时读写同一份共享资源,就会产生资源竞争。

临界资源:多个线程都能访问的公共资源,如全局变量、队列、文件。 临界区:访问临界资源的那一段代码。

示例问题:两个线程同时对同一个全局变量做自增,最终结果会出现错误,因为读‑改‑写操作被 CPU 调度打断。

解决竞争两类方案:

  1. 互斥:同一时刻只允许一个线程访问临界资源(互斥锁)

  2. 同步:控制多个线程执行先后顺序(信号量、条件变量)

1. 互斥锁 pthread_mutex_t

互斥锁实现排他访问临界资源。完整流程:

  1. 定义锁对象 pthread_mutex_t mutex;

  2. 初始化锁 pthread_mutex_init()

  3. 访问临界资源前加锁 pthread_mutex_lock()

  4. 操作公共数据

  5. 访问结束解锁 pthread_mutex_unlock()

  6. 不再使用销毁锁 pthread_mutex_destroy()

//初始化 pthread_mutex_init(&mutex, NULL); 
//临界区 pthread_mutex_lock(&mutex); 
//操作全局变量/队列 pthread_mutex_unlock(&mutex); 
//销毁 pthread_mutex_destroy(&mutex);

其他锁类型

  • 读写锁:读共享,写独占;适合读多写少场景

  • 自旋锁:拿不到锁时不睡眠,循环占用 CPU,适合锁持有时间很短场景

2. 信号量 sem_t(线程同步)

信号量可以控制线程执行顺序,实现同步。 P 操作sem_wait():信号量‑1,如果为 0 则阻塞等待; V 操作sem_post():信号量 + 1,唤醒等待线程。

API 清单

//初始化信号量 int sem_init(sem_t *sem, int pshared, unsigned int value);
 //P操作,申请资源 int sem_wait(sem_t *sem);
 //V操作,释放资源 int sem_post(sem_t *sem); 
//销毁 int sem_destroy(sem_t *sem);
  • pshared=0:用于线程之间;非 0 用于进程之间。

  • value:信号量初始资源计数。

四、多线程死锁

死锁:多个线程各自占有锁,又互相请求对方手里锁,全部永久阻塞。

死锁常见现象

  1. 忘记解锁;

  2. 同一个线程重复加同一把锁;

  3. 多把锁,线程加锁顺序不一致。

例:线程 A 持有锁 1,请求锁 2;线程 B 持有锁 2,请求锁 1,互相等待,死锁。

死锁四个必要条件,全部满足才会死锁,破坏任意一个即可避免死锁

  1. 互斥条件:资源同一时刻只能一个线程占有(锁本身特性)

  2. 请求与保持:线程已经占有锁,不释放,继续申请别的锁

  3. 不可剥夺:锁只能持有者主动释放,别人不能强制抢走

  4. 循环等待:多个线程形成环路,互相等待对方占有的资源

避免死锁的手段

  1. 锁成对使用:加锁后一定要解锁;

  2. 所有线程加锁顺序保持一致,破坏循环等待;

  3. 使用非阻塞接口:pthread_mutex_trylock(),拿不到锁主动释放已经获取资源,不阻塞等待。

五、线程开发小结

  1. 线程共享进程资源,创建开销远小于进程,但是线程崩溃会把整个进程带走;

  2. 默认非分离线程,结束后必须 join 回收;不需要 join 就设置 detach 分离属性;

  3. 多线程访问共享数据,必须加互斥锁保护临界区,防止数据错乱;

  4. 信号量实现线程执行顺序同步;

  5. 多锁场景注意死锁四大条件,重点统一加锁顺序。

Logo

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

更多推荐