线程同步【Linux操作系统】

发布于:2025-07-03 ⋅ 阅读:(20) ⋅ 点赞:(0)

线程同步

同步的引入

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

例:
一个死循环抢票的代码
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类型的结构体

分为

  • 局部条件变量[在局部定义的条件变量]

    • 1.只能使用pthread_cond_init初始化
    • 2.并且需要使用pthread_cond_destroy销毁局部条件变量
    • 3.条件变量是局部的,所以要让所有线程都看到的话,就需要把条件变量的地址/引用传给所有线程
  • 全局条件变量[在全局定义的条件变量]
    全局条件变量可以使用pthread_cond_init或者宏PTHREAD_COND_INITIALIZER初始化
    全局条件变量销不销毁无所谓,因为生命周期本来就和进程一样长


库函数:pthread_cond_init

  • 头文件:pthread.h

  • 参数表:

  • pthread_cond_t*mu:要初始化的条件变量的地址

  • const pthread_condattr_t*:用户指定的条件变量的属性,一般不管,设置为nullptr

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


库函数:pthread_cond_destroy

  • 头文件:pthread.h

  • 参数表:
    pthread_cond_t*mu:要销毁的条件变量的地址

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


库函数:pthread_cond_wait

  • 头文件:pthread.h

  • 参数表:

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

  • 注意:

    • ①线程执行pthread_cond_wait函数时,在进入条件变量的等待队列之前,会让线程解一次锁(解的是传进来的锁),不然拿着锁去阻塞了,就很可能会死锁

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


库函数:pthread_cond_signal

  • 头文件:pthread.h

  • 参数表:
    pthread_cond_t*cond:指定的条件变量的地址

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

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

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

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


库函数:pthread_cond_broadcast

  • 头文件:pthread.h

  • 参数表:
    pthread_cond_t*cond:指定的条件变量的地址

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



条件变量的使用要点

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

  • 1.因为条件变量本身也是共享资源,也需要被保护
    即:需要使用锁保证条件变量的wait函数具有“原子性”,pthread_cond_wait需要原子性的释放锁并进入等待队列等待
    不然调用pthread_cond_wait都有线程安全问题

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

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

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

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


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

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

但是我们可以通过让线程满足某些条件时,进入条件变量的等待队列
以及满足某些条件时,唤醒条件变量的等待队列中的线程
一定程度上做到操纵线程的调用顺序


伪唤醒问题的解决

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

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

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

此时:

  • 1.如果此时是使用if进行“线程是否需要进入条件变量的等待队列"的判断的这个被伪唤醒的线程,重新申请并拿到锁之后,就直接"饿虎出笼"去肆意妄为了

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

虽然循环条件是"线程需要进入等待队列"的条件,所以如果这个条件满足,不就意味着线程不应该被唤醒吗?


网站公告

今日签到

点亮在社区的每一天
去签到