前言
在 Linux C 多线程开发中,默认创建出来的线程是可连接属性(joinable) ,这种线程在线程退出之后,它的资源(线程栈、线程描述符)不会自动释放,必须调用pthread_join()去回收资源;如果不 join 就会产生僵尸线程,造成内存资源泄漏。 而线程分离属性 (detach) 就是用来解决这个问题的。本篇博客基于 Linux 下 pthread 库,讲解线程分离属性、多进程与多线程对比、线程间通信、互斥锁、死锁、信号量相关知识点。
环境:Ubuntu,gcc 编译需要增加
-pthread链接选项
一、线程分离属性 detachstate
线程设置分离属性之后:线程结束,系统自动回收线程资源,不需要手动调用 pthread_join 回收线程空间。
有两套 API,一套初始化属性结构体,创建线程的时候直接带上分离属性;另一套是线程创建完成后调用pthread_detach。这里讲解通过属性结构体设置分离属性的接口。
1. 相关函数原型
// 初始化线程属性结构体
int pthread_attr_init(pthread_attr_t *attr);
// 销毁线程属性结构体
int pthread_attr_destroy(pthread_attr_t *attr);
// 设置线程的分离状态属性
int pthread_attr_setdetachstate(pthread_attr_t *attr, int detachstate);
参数说明:
attr:线程属性变量的地址detachstate支持两个宏:
PTHREAD_CREATE_JOINABLE:可连接属性(默认),线程退出后需要pthread_join回收资源。PTHREAD_CREATE_DETACHED:分离属性,线程结束系统自动回收资源,禁止调用pthread_join。
**返回值:**成功返回 0;失败返回错误码。
2. 使用示例:创建分离属性线程
#include <stdio.h>
#include <pthread.h>
#include <unistd.h>
void *thread_func(void *arg)
{
printf("子线程运行,tid = %lu\n",pthread_self());
sleep(2);
printf("子线程即将退出\n");
pthread_exit(NULL);
}
int main(void)
{
pthread_t tid;
pthread_attr_t attr;
//1.初始化属性
pthread_attr_init(&attr);
//2.设置为分离属性
pthread_attr_setdetachstate(&attr,PTHREAD_CREATE_DETACHED);
//3.创建线程,带上分离属性
pthread_create(&tid,&attr,thread_func,NULL);
//4.销毁属性结构体,创建完线程之后属性结构体就可以销毁
pthread_attr_destroy(&attr);
sleep(3);
printf("主线程结束\n");
return 0;
}
编译命令:
gcc test.c -o test -pthread
./test
注意:分离属性线程千万不要调用
pthread_join,调用会返回失败。
补充:也可以在线程创建完成之后,调用pthread_detach(tid);,效果等价。
二、多进程 VS 多线程优缺点对比
表格
| 对比维度 | 多进程 | 多线程 |
|---|---|---|
| 调度开销 | 进程是操作系统资源分配最小单元,创建、切换开销大 | 线程是操作系统任务调度最小单元,创建切换开销更小,效率更高 |
| 内存空间 | 进程拥有独立完整 0‑4G 虚拟地址空间,进程之间完全隔离 | 线程共享进程的文本段、全局 / 静态变量、堆;每个线程仅拥有自己独立栈空间 |
| 健壮性(安全性) | 高,一个进程崩溃,不会影响其他进程 | 低,同一个进程内任意一个线程崩溃,整个进程直接退出,全部线程销毁 |
| 通信方式 | 进程地址空间隔离,必须使用 IPC 机制:管道、消息队列、共享内存、信号量、socket | 简单,直接共享全局 / 静态变量即可通信;但是会带来资源竞争问题,需要互斥同步 |
| 开发难度 | 通信复杂 | 共享变量简单,但是要处理锁、死锁问题 |
- 运行效率:多线程 > 多进程,同一进程内部调度不需要切换页表,开销小。
- 程序稳定性:多进程 > 多线程。
- 通信便捷性:多线程 > 多进程。
注意:线程的局部变量存放在线程自己的栈空间,线程私有;全局变量、静态变量、堆上 malloc 的内存所有线程共享。
三、线程间通信
1. 通信方式
线程之间最简单的通信手段:全局变量 / 静态变量,多个线程都可以访问同一块内存。
问题:多个线程同时读写同一个共享变量,就会出现资源竞争,结果错乱,需要引入互斥锁解决竞争。
概念科普
- 原子操作:不能被 CPU 任务调度打断的最小操作,一旦开始执行,就会执行完毕,不会中途切走 CPU。
- 临界区(临界代码):访问共享资源的代码片段,这段代码同一时刻只允许一个线程执行,一般放在加锁和解锁中间。
四、互斥锁 pthread_mutex_t
互斥锁作用:保证临界区同一时间只有一个线程执行,解决多线程资源竞争。
函数接口
//初始化互斥锁
int pthread_mutex_init(pthread_mutex_t *restrict mutex,
const pthread_mutexattr_t *restrict attr);
//销毁互斥锁
int pthread_mutex_destroy(pthread_mutex_t *mutex);
//上锁,拿不到锁就阻塞等待
int pthread_mutex_lock(pthread_mutex_t *mutex);
//解锁
int pthread_mutex_unlock(pthread_mutex_t *mutex);
attr锁属性,传 NULL 代表默认属性。- 返回值:成功返回 0,失败返回错误码。
简单使用范式:
pthread_mutex_t mutex;
pthread_mutex_init(&mutex,NULL);
//临界区前上锁
pthread_mutex_lock(&mutex);
//访问共享资源,临界区代码
pthread_mutex_unlock(&mutex);
pthread_mutex_destroy(&mutex);
五、死锁
死锁:多个线程互相拿着对方想要的锁,全部阻塞,谁也无法继续往下执行。
死锁产生 4 个必要条件,必须全部满足才会死锁
- 互斥条件:锁是互斥资源,同一时间只能一个线程持有锁。
- 不可剥夺条件:锁只能持有者主动释放,别人不能强行抢锁。
- 请求保持:线程已经持有一把锁,在不释放已有锁的前提下,申请另外一把锁。
- 循环等待:线程之间形成环路等待,A 拿锁 1 等锁 2;B 拿锁 2 等锁 1。
如何避免死锁
- 使用非阻塞加锁
pthread_mutex_trylock,拿不到锁不会阻塞,可以释放已经占有的锁。 - 多把锁的时候,所有线程加锁顺序完全保持一致,破坏循环等待条件。
- 尽量减少锁的数量,缩小临界区代码,锁粒度尽可能小。
六、信号量 sem_t
互斥锁只用于互斥;信号量既可以实现互斥,还可以做线程同步。信号量代表可用资源的数目。
函数接口
#include <semaphore.h>
//初始化信号量
int sem_init(sem_t *sem, int pshared, unsigned int value);
//销毁信号量
int sem_destroy(sem_t *sem);
//P操作:申请资源,资源>0就value-1;资源为0就阻塞等待
int sem_wait(sem_t *sem);
//V操作:释放资源,value+1,唤醒等待该信号量的线程
int sem_post(sem_t *sem);
参数说明:
sem:信号量变量地址pshared:0 代表用于同一个进程内部线程之间共享;非 0 代表进程间共享信号量。value:信号量初始资源数。
返回值:成功返回 0,失败返回‑1。
小技巧:信号量初始值设置为 1,就等价于互斥锁,可以完成互斥操作。
简单示例:信号量实现同步
sem_t sem;
sem_init(&sem,0,0);
//线程A
sem_post(&sem); //释放资源 +1
//线程B
sem_wait(&sem); //等待A释放资源,才继续往下执行
七、综合练习题(课后作业)
题目:实现 3 个线程任务,线程 1 循环打印 A,线程 2 循环打印 B,线程 3 循环打印 C,打印输出顺序固定为
ABCABCABC...
提示:可以使用 3 个信号量完成线程同步。
简单参考代码思路:
sem_t semA,semB,semC;
void *t1(void *arg)
{
while(1){
sem_wait(&semA);
printf("A");
fflush(stdout);
sem_post(&semB);
}
}
void *t2(void *arg)
{
while(1){
sem_wait(&semB);
printf("B");
fflush(stdout);
sem_post(&semC);
}
}
void *t3(void *arg)
{
while(1){
sem_wait(&semC);
printf("C ");
fflush(stdout);
sem_post(&semA);
}
}
int main()
{
pthread_t tid1,tid2,tid3;
sem_init(&semA,0,1);
sem_init(&semB,0,0);
sem_init(&semC,0,0);
pthread_create(&tid1,NULL,t1,NULL);
pthread_create(&tid2,NULL,t2,NULL);
pthread_create(&tid3,NULL,t3,NULL);
pthread_join(tid1,NULL);
pthread_join(tid2,NULL);
pthread_join(tid3,NULL);
sem_destroy(&semA);
sem_destroy(&semB);
sem_destroy(&semC);
return 0;
}
总结
- 线程默认 joinable 属性,必须
pthread_join回收;设置分离属性,线程退出自动回收,无需 join。设置分离属性两种方式:创建线程时设置 attr;或者pthread_detach(tid)。 - 进程是资源分配最小单元,线程是调度最小单元;线程共享进程大部分内存,栈是线程私有。
- 多线程访问共享全局变量会产生资源竞争,需要互斥锁保护临界区。
- 死锁 4 个必要条件,开发多锁代码要注意加锁顺序。
- 信号量 sem_t,P 操作
sem_wait申请资源,V 操作sem_post释放资源;初始为 1 可以当互斥锁,也可以做线程同步。