📝引言

进程是操作系统资源分配的最小单位,但是进程创建、切换开销大。线程作为轻量级进程,是 CPU 任务调度的基本单元,同一个进程内多个线程共享进程绝大多数资源,线程切换开销远小于进程。本篇讲解 Linux 下线程基础、线程创建回收、线程属性、线程互斥锁,帮助掌握多线程并发编程

1. 什么是线程

线程是一个轻量级的进程,用来实现多任务并发。 线程是操作系统任务调度的最小单位;进程是操作系统资源分配的最小单位。

线程由所在进程创建,线程一定隶属于某个进程。 线程创建的时候,操作系统会为每一个线程分配独立栈空间(默认 8M);同一个进程内所有线程共享进程的堆区、数据段、bss 段、代码段、内核空间,仅栈区互相独立

2. 进程与线程对比

对比项进程线程
核心概念操作系统资源分配的最小单位CPU 任务调度的最小单位
创建切换效率创建开销大,完整复制虚拟内存;切换速度慢创建快,仅分配栈;线程间切换速度快
内存占用完整 0‑4G 虚拟地址空间,内存消耗大,并发数量受限仅拥有独立栈区,其余内存和进程共享,资源开销小
安全性进程之间地址空间完全独立,一个进程崩溃不影响其他进程共享进程地址空间,任意一个线程崩溃会导致整个进程全部退出
通信方式不能直接访问,必须使用 IPC 进程间通信机制同进程线程直接读写全局变量、堆内存即可完成通信

3. 线程调度

宏观并行,微观串行。CPU 时间片轮转调度,调度算法和进程保持一致。

4. 线程消亡

  1. 线程执行函数 return 返回,线程结束
  2. 整个进程退出,进程内所有线程全部直接消亡
  3. 调用pthread_exit()主动退出当前线程

5. 线程常用 API

5.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)

⚠️编译注意:线程库不属于标准 C 库,编译链接必须加 -pthread。主线程不能在线程执行完毕前直接退出,否则子线程直接销毁。

5.2 线程分离属性

线程默认是非分离属性:线程退出之后资源不会自动释放,必须其他线程调用pthread_join回收;如果不回收会产生资源泄露。

  • pthread_detach(tid):设置线程为分离属性;线程结束操作系统自动回收资源,不需要 join 回收。 适用场景:主线程没有机会去阻塞等待子线程结束。
5.3 pthread_join 阻塞回收线程
int pthread_join(pthread_t thread, void **retval);
  • 功能:阻塞等待指定线程结束,回收线程资源
  • 参数
    • thread:待回收线程 ID
    • retval:接收线程返回值,不关心返回值填 NULL
  • 返回值:成功 0,失败非 0

6. 线程同步互斥:互斥锁

多线程并发访问临界资源(全局变量、队列、文件、数据库)会产生资源竞争,数据错乱。

临界资源:多个线程都可以访问的资源;访问临界资源的代码片段称为临界区。

互斥锁作用:保证同一时间,只有一个线程进入临界区访问资源,实现访问排他性。

互斥锁操作流程:

  1. 定义锁对象:pthread_mutex_t mutex;
  2. 初始化锁:pthread_mutex_init()
  3. 访问临界资源前加锁:pthread_mutex_lock()
  4. 访问结束解锁:pthread_mutex_unlock()
  5. 不再使用销毁锁:pthread_mutex_destroy()
int pthread_mutex_init(pthread_mutex_t *restrict mutex,
const pthread_mutexattr_t *restrict attr);
  • mutex:锁对象地址
  • attr:锁属性,NULL 使用默认属性
  • 返回:成功 0,失败错误码

拓展其他锁类型:

  1. 读写锁:读共享,写独占;适合读多写少场景
  2. 自旋锁:获取不到锁不会休眠,持续占用 CPU 循环轮询

7. 信号量实现线程同步

信号量可以控制访问资源线程数量,实现线程同步。

int sem_init(sem_t *sem, int pshared, unsigned int value);
  • sem:信号量对象
  • pshared:0 代表线程间共享;非 0 代表进程间共享
  • value:信号量初始资源计数 返回:成功 0,失败非 0

信号量配套接口

  • sem_wait():P 操作,资源计数‑1;资源为 0 时阻塞等待
  • sem_post():V 操作,资源计数 + 1,唤醒等待线程
  • sem_destroy():销毁信号量

8. 死锁产生条件以及解决

死锁:多线程场景,各个线程已经持有部分锁资源,同时互相请求对方持有的锁,全部永久阻塞。

死锁 4 个必要条件,破坏任意一条即可消除死锁

  1. 互斥条件:资源同一时刻只能被一个线程占有(锁本身特性)
  2. 请求与保持条件:线程已经占有锁,同时申请新锁,原有锁不释放
  3. 不剥夺条件:锁只能持有者主动释放,其他线程不能强制抢夺锁
  4. 循环等待条件:多个线程形成环形互相等待锁资源

常见死锁场景:忘记解锁;重复加锁;多把锁加锁顺序混乱。

解决思路:

  1. 加锁解锁必须成对出现
  2. 多线程操作多把锁,所有线程加锁顺序保持完全一致,破坏循环等待
  3. 使用非阻塞锁pthread_mutex_trylock()sem_trywait(),申请失败主动释放已经获取资源
Logo

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

更多推荐