Days 32 Linux 多线程:线程分离属性、多进程 vs 多线程、线程同步互斥锁、信号量

前言

在 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);

参数说明:

  1. attr:线程属性变量的地址
  2. 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 简单,直接共享全局 / 静态变量即可通信;但是会带来资源竞争问题,需要互斥同步
开发难度 通信复杂 共享变量简单,但是要处理锁、死锁问题
  1. 运行效率:多线程 > 多进程,同一进程内部调度不需要切换页表,开销小。
  2. 程序稳定性:多进程 > 多线程
  3. 通信便捷性:多线程 > 多进程

注意:线程的局部变量存放在线程自己的栈空间,线程私有;全局变量、静态变量、堆上 malloc 的内存所有线程共享。

三、线程间通信

1. 通信方式

线程之间最简单的通信手段:全局变量 / 静态变量,多个线程都可以访问同一块内存。

问题:多个线程同时读写同一个共享变量,就会出现资源竞争,结果错乱,需要引入互斥锁解决竞争。

概念科普

  1. 原子操作:不能被 CPU 任务调度打断的最小操作,一旦开始执行,就会执行完毕,不会中途切走 CPU。
  2. 临界区(临界代码):访问共享资源的代码片段,这段代码同一时刻只允许一个线程执行,一般放在加锁和解锁中间。

四、互斥锁 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 个必要条件,必须全部满足才会死锁

  1. 互斥条件:锁是互斥资源,同一时间只能一个线程持有锁。
  2. 不可剥夺条件:锁只能持有者主动释放,别人不能强行抢锁。
  3. 请求保持:线程已经持有一把锁,在不释放已有锁的前提下,申请另外一把锁。
  4. 循环等待:线程之间形成环路等待,A 拿锁 1 等锁 2;B 拿锁 2 等锁 1。

如何避免死锁

  1. 使用非阻塞加锁pthread_mutex_trylock,拿不到锁不会阻塞,可以释放已经占有的锁。
  2. 多把锁的时候,所有线程加锁顺序完全保持一致,破坏循环等待条件。
  3. 尽量减少锁的数量,缩小临界区代码,锁粒度尽可能小。

六、信号量 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);

参数说明:

  1. sem:信号量变量地址
  2. pshared:0 代表用于同一个进程内部线程之间共享;非 0 代表进程间共享信号量。
  3. 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;
}

总结

  1. 线程默认 joinable 属性,必须pthread_join回收;设置分离属性,线程退出自动回收,无需 join。设置分离属性两种方式:创建线程时设置 attr;或者pthread_detach(tid)
  2. 进程是资源分配最小单元,线程是调度最小单元;线程共享进程大部分内存,栈是线程私有。
  3. 多线程访问共享全局变量会产生资源竞争,需要互斥锁保护临界区。
  4. 死锁 4 个必要条件,开发多锁代码要注意加锁顺序。
  5. 信号量 sem_t,P 操作sem_wait申请资源,V 操作sem_post释放资源;初始为 1 可以当互斥锁,也可以做线程同步。
相关推荐
李少兄18 分钟前
Linux 服务器从零部署 kkFileView 完全指南(Docker 方案)
linux·服务器·docker
小张同学a.20 分钟前
OpenStack 私有云实战 3—— Neutron 网络服务与 Dashboard 控制台部署
linux·运维·服务器·openstack
DogDaoDao21 分钟前
HarmonyOS 深度解析:从微内核到 ArkTS 工程实战
android·linux·ios·华为·程序员·harmonyos·harmonyos next
从零开始的嵌入式之旅26 分钟前
day31
linux·c语言·经验分享·笔记·嵌入式硬件
小张帅三代29 分钟前
yolo基础学习笔记
笔记·学习·yolo
北风toto37 分钟前
UML(统一建模语言)详细笔记
笔记·uml·软件设计师
Horn Still Sounds39 分钟前
Linux系统编程进阶:进程回收、程序替换与多线程并发模型
linux·运维·服务器·c语言
ltl40 分钟前
rsync 协议与进程模型:generator、sender 与 receiver
linux
ouynagda1 小时前
Linux 进程与线程学习笔记
linux·笔记·学习