Linux应用软件编程——线程还不会?快看这篇内容全讲解!
本文基于 Linux C 应用开发学习笔记,对比进程,讲解线程原理、API、线程同步互斥、死锁问题。
一、什么是线程
线程是轻量级进程,用来实现程序多任务并发。
-
✅ 进程:操作系统资源分配最小单位
-
✅ 线程:CPU 任务调度最小单位
线程一定隶属于某个进程,一个进程内部可以创建多个线程。
进程与线程内存模型
进程创建的时候分配完整 0‑4G 虚拟地址空间; 同一个进程内所有线程共享进程大部分资源,仅拥有自己独立栈区(默认 8M):
-
共享:堆区、
.data段、.bss段、字符串常量区、代码文本区、内核空间 -
私有:每个线程独立栈区,保存局部变量、函数调用现场。
优势:线程创建、切换开销远小于进程;缺点:共享资源带来竞争风险。
进程 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);
参数说明
-
thread:输出参数,保存线程 ID -
attr:线程属性,NULL使用默认属性 -
start_routine:线程执行函数,函数格式必须void* func(void*) -
arg:传递给线程函数的参数
返回值:成功返回 0;失败返回非 0 错误码。
2. 线程退出
线程退出有三种方式:
-
线程任务函数
return返回; -
调用
pthread_exit(void *retval)主动退出线程; -
整个进程退出,进程内所有线程全部销毁。
⚠️重要:主线程不要提前 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. 互斥锁 pthread_mutex_t
互斥锁实现排他访问临界资源。完整流程:
-
定义锁对象
pthread_mutex_t mutex; -
初始化锁
pthread_mutex_init() -
访问临界资源前加锁
pthread_mutex_lock() -
操作公共数据
-
访问结束解锁
pthread_mutex_unlock() -
不再使用销毁锁
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:信号量初始资源计数。
四、多线程死锁
死锁:多个线程各自占有锁,又互相请求对方手里锁,全部永久阻塞。
死锁常见现象
-
忘记解锁;
-
同一个线程重复加同一把锁;
-
多把锁,线程加锁顺序不一致。
例:线程 A 持有锁 1,请求锁 2;线程 B 持有锁 2,请求锁 1,互相等待,死锁。
死锁四个必要条件,全部满足才会死锁,破坏任意一个即可避免死锁
-
互斥条件:资源同一时刻只能一个线程占有(锁本身特性)
-
请求与保持:线程已经占有锁,不释放,继续申请别的锁
-
不可剥夺:锁只能持有者主动释放,别人不能强制抢走
-
循环等待:多个线程形成环路,互相等待对方占有的资源
避免死锁的手段
-
锁成对使用:加锁后一定要解锁;
-
所有线程加锁顺序保持一致,破坏循环等待;
-
使用非阻塞接口:
pthread_mutex_trylock(),拿不到锁主动释放已经获取资源,不阻塞等待。
五、线程开发小结
-
线程共享进程资源,创建开销远小于进程,但是线程崩溃会把整个进程带走;
-
默认非分离线程,结束后必须 join 回收;不需要 join 就设置 detach 分离属性;
-
多线程访问共享数据,必须加互斥锁保护临界区,防止数据错乱;
-
信号量实现线程执行顺序同步;
-
多锁场景注意死锁四大条件,重点统一加锁顺序。
openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构
更多推荐



所有评论(0)