当前位置: 首页 > news >正文

Linux——线程同步

文章目录

  • 一、同步
    • 同步的引入
    • 同步和互斥存在的意义
    • 条件变量的理解
  • 二、条件变量相关接口
    • 条件变量的结构体
    • 初始化
    • 销毁
    • 等待
    • 唤醒第一个线程
    • 唤醒所有线程
  • 三、条件变量的使用要点
    • 线程一定是在临界区中进入条件变量的等待队列的
    • 可以通过条件变量控制线程运行的顺序
    • 伪唤醒问题的解决

一、同步

同步的引入

如果一个共享资源只加了锁,就有可能出现锁的饥饿问题

例:
一个死循环抢票的代码
while(1)
{
//加锁
p–
//解锁
}
一个线程a抢到锁之后,其他线程想要锁的进程就只能阻塞等待线程a解锁
线程a使用完临界区之后,解锁之后,又进入下一次循环,又去抢锁了
因为其他想抢锁的线程还阻塞着,唤醒需要时间
但是线程a本来就醒着,所以线程a就比别的线程快,马上又把锁抢到了
其他想要锁的线程只能再次进入阻塞状态

就有可能一直是线程a拿着锁,访问临界区

怎么解决这个问题?
就要用到同步
即:

  • 规定所有没有获取到锁的,又想要锁的,阻塞时必须排队

  • 获取了锁的线程,执行完临界区代码并解锁之后,即使还要想获取锁,也必须排在队伍最末尾

同步和互斥存在的意义

互斥是为了保护共享资源,防止出现数据不一致问题
但是互斥并不能保证临界区代码高效合理

同步则是作为互斥的补充
同步保证多执行流时,执行流在访问共享资源时具有一定的顺序性
同步是在互斥的基础(共享资源安全)之上,保障临界区代码的高效合理

条件变量的理解

举例子来理解:
有n+1个人闲的没事干,一起玩"拿苹果和放苹果"的小游戏
游戏规则:

  • ①一个人负责往盘子里面放苹果

  • ②n个人蒙上眼睛,从盘子里面拿苹果

  • ③如果盘子里面有苹果,那放苹果的人就不会再放了

  • ④如果盘子里面没苹果,那么拿苹果的人可能再次检测盘子看它是否有苹果

  • ⑤盘子被加了互斥锁,即同时只能有一个人去使用盘子

如果没加同步
那么就会和引入时所说的,出现饥饿问题,即可能有一个蒙着眼睛的人没拿到苹果,就一直用手去摸盘子
导致放苹果的人放不了,就出现锁的饥饿问题了

所以他们商量了一下,把规则改成了:

  • ①一个蒙眼拿苹果的人如果没有拿到苹果,就不能再用手去摸盘子了,必须去等待队列中阻塞等待
    即:条件不具备,就阻塞

  • ②如果蒙眼的人都去阻塞等了,那就没人来拿苹果了呀?
    所以给放苹果的人一个铃铛,如果他往盘子里面放了苹果,那么就敲一下铃铛
    此时等待队列中的第一个人才可以去拿苹果
    即:条件具备了,就唤醒

  • ③拿到苹果的人,如果还想拿苹果,就得排到等待队列末尾

所以
如果我们把

  • ①所有的人都是线程
  • ②锁就是锁
  • ③盘子就是临界资源
  • ④苹果就是数据
  • ⑤铃铛+等待队列就是条件变量
  • ⑥让蒙眼的人去等待队列等这个操作就是pthread_cond_wait
  • ⑦敲一下铃铛这个操作就是pthread_cond_signal
  • ⑧敲n下铃铛就是pthread_cond_broadcast

所以条件变量是:
条件变量是一个用来实现线程同步的特性,内部会维护一个等待队列
相当与是一个:提示器+等待队列

二、条件变量相关接口

条件变量的结构体

pthread_cond_t类型的结构体

分为

全局条件变量

  • 全局条件变量可以使用pthread_cond_init或者宏PTHREAD_COND_INITIALIZER初始化
  • 全局条件变量销不销毁无所谓,因为生命周期本来就和进程一样长
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;

局部条件变量

  • 只能使用pthread_cond_init初始化
  • 并且需要使用pthread_cond_destroy销毁局部条件变量
  • 条件变量是局部的,所以要让所有线程都看到的话,就需要把条件变量的地址/引用传给所有线程

初始化

pthread_cond_init

作用:初始化对应的条件变量

#include <pthread.h>int pthread_cond_init(pthread_cond_t * cond,  const pthread_condattr_t * attr);    
  • pthread_cond_t* cond:要初始化的条件变量的地址
  • const pthread_condattr_t* attr:用户指定的条件变量的属性,一般不管,设置为nullptr
  • 返回值
    0 成功
    非 0 失败

销毁

pthread_cond_destroy

作用:销毁对应的条件变量

int pthread_cond_destroy(pthread_cond_t *cond);
  • pthread_cond_t* cond:要销毁的条件变量的地址
  • 返回值
    0 成功
    非 0 失败

等待

pthread_cond_wait

作用:使用指定的互斥锁保障线程安全,并让调用该函数的线程,在指定的条件变量的等待队列中阻塞等待

int pthread_cond_wait(pthread_cond_t * cond,pthread_mutex_t * mutex);
  • pthread_cond_t*cond:指定的条件变量的地址
  • pthread_mutex_t*mutex:指定的互斥锁的地址
  • 返回值
    0 成功
    非 0 失败

注意:

  • 线程执行pthread_cond_wait函数时,在进入条件变量的等待队列之前,会让线程解一次锁(解的是传进来的锁),不然拿着锁去阻塞了,就很可能会死锁
  • 当条件变量的等待队列中的线程被唤醒时,线程继续执行pthread_cond_wait中剩余的代码时,因为出了wait函数就是临界区,所以必须再申请一次锁(申请的是传进来的锁)才能出pthread_cond_wait函数,互斥地进入临界区
    此时如果锁被其他线程拿走了,线程就又会阻塞,只不过此时线程是在锁的等待队列中阻塞的,而不是条件变量的等待队列

唤醒第一个线程

pthread_cond_signal

作用:唤醒指定条件变量的等待队列中的队头线程(第一个线程

int pthread_cond_signal(pthread_cond_t *cond);
  • pthread_cond_t*cond:指定的条件变量的地址
  • 返回值
    0 成功
    非 0 失败

为什么唤醒一般是唤醒队头线程?而不是随机唤醒?

  • 唤醒队头线程的时间复杂度一般情况下比随机唤醒低 而且唤醒队头线程确定性更高,更好调试

  • 在等待队列队头的线程,可以理解为最先进入等待队列的线程,即等待时间最长的线程
    为了防止饥饿,它理应被最先唤醒

唤醒所有线程

pthread_cond_broadcast

作用:唤醒指定条件变量的等待队列中的所有线程

int pthread_cond_broadcast(pthread_cond_t *cond);
  • pthread_cond_t*cond:指定的条件变量的地址
  • 返回值
    0 成功
    非 0 失败

三、条件变量的使用要点

线程一定是在临界区中进入条件变量的等待队列的

  • 因为条件变量本身也是共享资源,也需要被保护

    即:需要使用锁保证条件变量的wait函数具有“原子性”,pthread_cond_wait需要原子性的释放锁并进入等待队列等待
    不然调用pthread_cond_wait都有线程安全问题

  • 因为进入条件变量的等待队列是有条件的,又因为条件变量是实现多线程间同步的

    所以这多个线程要满足的条件(看到的条件)是一样的,即条件本身就是共享资源/共享资源的一部分
    即判断这个条件是否成立,一定需要访问共享资源
    所以线程调用pthread_cond_wait的时候,一定是拿着锁的 所以为了防止拿着锁的线程,去条件变量的等待队列下等待,造成死锁
    所以:

    • pthread_cond_wait才会在自己的函数体中先解锁,再把线程放进等待队列,这个是为了防止死锁

    • pthread_cond_wait才会在线程被唤醒之后,再让线程申请一次锁,这个是为了恢复等待之前线程持有锁的状态[因为线程从条件变量的等待队列中醒来时,还在临界区中]

  • 条件变量的wait函数的实现,就会释放锁和申请锁
    所以:保护共享的条件的时候,必须得用锁!没法用其他保护手段

可以通过条件变量控制线程运行的顺序

在一次创建了多个线程之后,线程的调度顺序如何我们并不清楚,因为这完全是由操作系统自主决定的

但是我们可以通过让线程满足某些条件时,进入条件变量的等待队列
以及满足某些条件时,唤醒条件变量的等待队列中的线程

一定程度上做到操纵线程的调用顺序

伪唤醒问题的解决

即: 判断线程是否要进入条件变量的等待队列时,判断不能用if而 要用while

不然就有可能出现伪唤醒问题:即在条件变量下等待的线程,唤醒条件其实并不满足,但是因为程序员编码的问题,可能意外被唤醒了

例如:
生产者消费者模型中,因为阻塞队列中没有数据,所以全部都5个消费者线程在条件变量的等待队列中等待
生产者线程生产了一个数据,意外地把唤醒了多个消费者线程
然后一个消费者线程抢到锁之后,把阻塞队列中那唯一的一个数据抢走了,它解锁之后
因为唤醒了多个消费者线程
所以锁可能又被一个消费者线程抢到了,但是此时阻塞队列中根本没有数据!

此时:

  • 如果此时是使用if进行“线程是否需要进入条件变量的等待队列"的判断的这个被伪唤醒的线程,重新申请并拿到锁之后,就直接"饿虎出笼"去肆意妄为了
  • 如果是使用while进行“线程是否需要进入条件变量的等待队列”的判断的,这个被唤醒的线程,重新申请并拿到锁之后,也还是不能直接出循环,因为要再判断一下循环条件是否不满足了

虽然循环条件(如 while (queue.empty()))表示‘线程需要等待’,但被唤醒后必须重新检查条件,因为唤醒操作本身不保证条件已满足。

http://www.xdnf.cn/news/1193221.html

相关文章:

  • CT、IT、ICT 和 DICT区别
  • 【架构】Docker简单认知构建
  • 【科研绘图系列】R语言绘制误差连线散点图
  • 秋招Day19 - 分布式 - 分布式事务
  • 生产环境使用云服务器(centOS)部署和使用MongoDB
  • Java操作Excel文档
  • opencv学习(图像金字塔)
  • 背包问题及 LIS 优化
  • 告别配置混乱!Spring Boot 中 Properties 与 YAML 的深度解析与最佳实践
  • C#编程基础:运算符与结构详解
  • 【Android】相对布局应用-登录界面
  • 2025.7.26字节掀桌子了,把coze开源了!!!
  • window下MySQL安装(三)卸载mysql
  • Fast_Lio 修改激光雷达话题
  • VLAN的划分(基于华为eNSP)
  • MySQL 8.0 OCP 1Z0-908 题目解析(37)
  • 尝试几道算法题,提升python编程思维
  • Linux内核设计与实现 - 课程大纲
  • LeetCode 1074:元素和为目标值的子矩阵数量
  • 使用Spring Boot创建Web项目
  • 学习嵌入式的第三十二天-数据结构-(2025.7.24)IO多路复用
  • 开发者说|RoboTransfer:几何一致视频世界模型,突破机器人操作泛化边界
  • 1. Qt多线程开发
  • SpringMVC——建立连接
  • OpenFeign-远程调用
  • 计算机中的数据表示
  • Windows Server系统安装JDK,一直卡在“应用程序正在为首次使用作准备,请稍候”
  • Java程序员学从0学AI(六)
  • 框架式3D打印机结构设计cad【9张】三维图+设计说明书
  • openmv特征点检测