欢迎光临
我们一直在努力

【Linux 系统编程】学习第七天:线程属性 | 互斥锁 | 死锁 | 信号量 核心知识点 + 踩坑实战

文章目录

  • 前言
  • 一、线程分离属性 pthread_attr_t
    • 1. 核心 API
    • 2. 使用流程
    • 3. 易错点
  • 二、多进程 VS 多线程 全方位对比
    • 适用场景总结
  • 三、线程间通信与临界区概念
    • 1. 线程通信方式
    • 2. 带来的问题:资源竞争
    • 3. 核心概念
    • 4. 经典误区:一个加锁一个不加,锁有用吗?
  • 四、互斥锁 pthread_mutex
    • 1. 核心 API
    • 2. 标准使用模板
    • 3. 补充:非阻塞加锁
  • 五、死锁
    • 1. 什么是死锁
    • 2. 死锁产生的四个必要条件
    • 3. 死锁规避方案
  • 六、信号量 sem_t
    • 1. 核心 API
    • 2. 信号量的两种经典用法
    • 3. 实战案例:三个线程按序打印 A、B、C
  • 七、实战踩坑合集
    • 1. 输入函数坑:scanf ("% s") 缓冲区残留
    • 2. 信号量初值坑
    • 3. 主线程退出坑
    • 4. 属性销毁坑
  • 学习总结

前言

今天是系统编程学习的第七天,核心围绕多线程同步与通信展开,覆盖线程分离属性、多进程与多线程的优劣对比、互斥锁机制、死锁的产生与规避、信号量的同步 / 互斥用法四大模块。

学习过程中踩了不少经典面试坑:比如信号量初值设置错误导致线程顺序错乱、误以为互斥锁是硬件级变量保护、scanf 输入带空格字符串出现缓冲区残留等。本篇把知识点、API、实战案例、踩坑避坑一次性整理清楚,方便复习。


一、线程分离属性 pthread_attr_t

线程默认是可接合状态(PTHREAD_CREATE_JOINABLE),线程退出后系统不会自动回收资源,必须由主线程调用 pthread_join() 阻塞等待并回收;而分离状态(PTHREAD_CREATE_DETACHED)的线程,结束后系统自动回收资源,无需也不能调用 join。

1. 核心 API

函数功能
pthread_attr_init 初始化线程属性结构体,必须先初始化再使用
pthread_attr_setdetachstate 设置线程分离状态
pthread_attr_destroy 销毁属性对象,释放内部资源
函数原型详解

// 初始化属性,设置默认值
int pthread_attr_init(pthread_attr_t *attr);

// 设置分离状态
// detachstate 取值:
// PTHREAD_CREATE_JOINABLE 默认,可接合
// PTHREAD_CREATE_DETACHED 分离态
int pthread_attr_setdetachstate(pthread_attr_t *attr, int detachstate);

// 销毁属性
int pthread_attr_destroy(pthread_attr_t *attr);

2. 使用流程

pthread_attr_t attr;
pthread_attr_init(&attr); // 1.初始化
pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED); // 2.设置分离

pthread_create(&tid, &attr, thread_func, NULL); // 3.创建线程,传入属性

pthread_attr_destroy(&attr); // 4.销毁属性(不影响已创建的线程)

3. 易错点

  • 属性只是创建模板:线程创建完成后,属性就和线程无关了,此时 destroy 属性不会影响正在运行的线程。
  • 分离态线程禁止调用 pthread_join,否则会出现未定义行为。
  • 除了创建时设置,也可以在线程函数内部调用 pthread_detach(pthread_self()) 将自身设为分离态。

  • 二、多进程 VS 多线程 全方位对比

    从四个核心维度对比:

    对比维度多线程多进程
    运行效率 ✅ 更高。同一地址空间,线程切换开销小,调度快 ❌ 更低。进程切换需要切换页表、刷新缓存,开销大
    安全性 ❌ 差。一个线程崩溃,整个进程退出,所有线程全部终止 ✅ 高。进程地址空间独立,单个进程崩溃不影响其他进程
    通信实现 ✅ 简单。共享全局变量、堆内存,直接读写即可通信 ❌ 复杂。地址空间隔离,必须通过管道、共享内存、消息队列等 IPC 机制
    编程复杂度 中等。共享资源会产生数据竞争,需要加锁保护 高。IPC 机制代码量大,调试难度高

    适用场景总结

    • 多线程:计算密集型任务、频繁数据交互、并发量高的场景(如服务器、线程池)
    • 多进程:高可靠性要求、多任务隔离、崩溃不能影响整体的场景(如浏览器、守护进程)

    三、线程间通信与临界区概念

    1. 线程通信方式

    同一进程的所有线程共享同一个虚拟地址空间,因此全局变量、静态变量、堆内存都是天然共享的,线程间可以直接通过这些共享变量通信。

    2. 带来的问题:资源竞争

    多个线程同时读写同一个共享变量时,会发生数据竞争,导致结果错乱。比如两个线程同时对全局变量执行 num++,最终结果会小于预期。

    3. 核心概念

    • 原子操作:不能被 CPU 调度打断的最小执行单元,执行过程不会被切走。
    • 临界区(临界代码):被加锁和解锁包裹的代码片段,同一时刻最多只能有一个线程进入执行。

    4. 经典误区:一个加锁一个不加,锁有用吗?

    完全没用,互斥锁直接失效。 互斥锁本质是软件层面的约定,不是硬件级的变量保护。pthread_mutex_lock 只能拦住调用了这个函数的线程;如果有一个线程读写共享变量时不加锁,它会直接无视锁的存在修改内存,数据竞争依然存在。

    铁律:所有访问同一共享资源的线程,必须使用同一把锁包裹临界区,只要漏一个,锁机制就作废。


    四、互斥锁 pthread_mutex

    互斥锁(Mutex)是解决多线程资源竞争最常用的工具,保证临界区代码的互斥执行。

    1. 核心 API

    函数功能
    pthread_mutex_init 初始化互斥锁,默认属性传 NULL
    pthread_mutex_destroy 销毁互斥锁,释放资源
    pthread_mutex_lock 加锁,拿不到锁则阻塞休眠
    pthread_mutex_unlock 解锁,唤醒等待的线程
    函数原型

    // 初始化锁,attr为NULL使用默认属性
    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);

    2. 标准使用模板

    pthread_mutex_t lock;
    pthread_mutex_init(&lock, NULL);

    // 线程内部
    pthread_mutex_lock(&lock);
    // ===== 临界区:访问共享资源 =====
    num = 100;
    printf("%d\\n", num);
    // =================================
    pthread_mutex_unlock(&lock);

    3. 补充:非阻塞加锁

    pthread_mutex_trylock():尝试加锁,拿不到锁不会阻塞,直接返回错误码,常用于避免死锁。


    五、死锁

    1. 什么是死锁

    多个线程互相持有对方需要的锁,同时又等待对方释放锁,导致所有线程永久阻塞,程序卡死,就是死锁。

    2. 死锁产生的四个必要条件

  • 互斥条件:资源同一时间只能被一个线程占用
  • 不可剥夺条件:已获得的锁不能被其他线程强行抢走
  • 请求保持:线程持有已有锁的同时,又去请求新的锁
  • 循环等待:线程之间形成环形的锁等待链
  • 四个条件同时满足才会产生死锁,破坏任意一个即可避免死锁。

    3. 死锁规避方案

  • 所有线程按照相同的顺序加锁(破坏循环等待)
  • 使用 pthread_mutex_trylock 尝试加锁,拿不到就释放已有锁(破坏请求保持)
  • 避免锁嵌套,减少锁的持有时间

  • 六、信号量 sem_t

    信号量本质是一个内核维护的计数器 + 等待队列,支持两种原子操作:P 操作(申请资源,计数 – 1)、V 操作(释放资源,计数 + 1)。既可以实现互斥,也可以实现线程同步。

    1. 核心 API

    函数功能
    sem_init 初始化信号量
    sem_destroy 销毁信号量
    sem_wait P 操作:申请资源,计数 – 1,计数为 0 则阻塞
    sem_post V 操作:释放资源,计数 + 1,唤醒等待线程
    函数原型详解

    // 初始化信号量
    // pshared: 0 = 本进程内线程间使用;非0 = 多进程间使用(需放在共享内存)
    // value: 信号量初始计数值
    int sem_init(sem_t *sem, int pshared, unsigned int value);

    // 销毁信号量
    int sem_destroy(sem_t *sem);

    // P操作:申请资源
    int sem_wait(sem_t *sem);

    // V操作:释放资源
    int sem_post(sem_t *sem);

    高频考点:sem_init 第二个参数 pshared

    • 写 0:用于同一进程的多个线程之间同步,信号量定义为全局变量即可
    • 写非 0:用于多个进程之间同步,信号量必须放在共享内存中

    2. 信号量的两种经典用法

    (1)二元信号量(初值 = 1):实现互斥

    等价于互斥锁,sem_wait 对应加锁,sem_post 对应解锁。

    (2)同步信号量(初值 = 0):控制执行顺序

    用于保证线程的先后执行顺序,A 线程做完事再让 B 线程执行。

    3. 实战案例:三个线程按序打印 A、B、C

    需求

    三个线程循环打印,严格按照 A → B → C 的顺序输出。

    错误写法踩坑
    • 坑 1:sem_init 第二个参数错传 1,信号量初始化异常
    • 坑 2:信号量初值设置错误,比如第三个信号量初值设为 1,导致 C 线程抢先执行
    正确实现

    // 信号量初值:sem_w=1, sem_r=0, sem_a=0
    sem_init(&sem_w, 0, 1);
    sem_init(&sem_r, 0, 0);
    sem_init(&sem_a, 0, 0);

    • sem_w=1:A 线程初始拥有执行许可,最先运行
    • sem_r=0、sem_a=0:B、C 线程初始无许可,阻塞等待前一个线程唤醒

    执行时序:

  • A 线程:sem_wait (sem_w) → 打印 A → sem_post (sem_r)
  • B 线程被唤醒:sem_wait (sem_r) → 打印 B → sem_post (sem_a)
  • C 线程被唤醒:sem_wait (sem_a) → 打印 C → sem_post (sem_w)
  • 循环往复,严格保持 A→B→C 顺序
  • 口诀:接力顺序打印,第一个线程信号量初值 = 1,后面全部初值 = 0。


    七、实战踩坑合集

    1. 输入函数坑:scanf ("% s") 缓冲区残留

    问题:密码验证题目中,输入 try again(带空格)时,scanf("%s") 遇到空格截断,只读取 try,剩下的 again 残留在输入缓冲区,程序退出后被 shell 读取,出现乱码输出。

    解决:题目要求按行输入时,使用 fgets() 读取整行,手动去掉换行符:

    fgets(str, sizeof(str), stdin);
    str[strcspn(str, "\\n")] = '\\0';

    2. 信号量初值坑

    同步场景下,后执行的线程对应的信号量初值必须为 0,否则线程会抢先执行,破坏顺序。

    3. 主线程退出坑

    主线程执行 return 结束进程时,所有子线程会被直接销毁,不管有没有执行完。所以需要用 pthread_join 或者死循环保证主线程不退出。

    4. 属性销毁坑

    pthread_attr_destroy 只是销毁属性对象,不会杀死已经创建好的线程。

    学习总结

    今天的核心是多线程同步,重点掌握:

  • 线程分离属性的用法和两种状态的区别
  • 多进程和多线程的优劣对比(面试必背)
  • 互斥锁的作用、临界区概念,以及 “锁是约定不是硬件保护” 的本质
  • 死锁的四个必要条件和规避方法
  • 信号量的 P/V 操作,以及互斥、同步两种用法,重点掌握顺序打印的初值设置
  • 赞(0)
    未经允许不得转载:171主机测评 » 【Linux 系统编程】学习第七天:线程属性 | 互斥锁 | 死锁 | 信号量 核心知识点 + 踩坑实战
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址