Linux系统编程--线程同步

devtools/2025/3/10 16:36:55/

目录

一、前言

二、线程饥饿

三、线程同步

四、条件变量

1、cond

2、条件变量的使用

五、条件变量与互斥锁


一、前言

        上篇文章我们讲解了线程互斥的概念,为了防止多个线程同时访问一份临界资源而出问题,我们引入了线程互斥,线程互斥其实就是多个线程同时争抢一份资源,谁抢到了就是谁的,抢不到的只能等待着下一次抢。虽然解决了有多个线程同时访问同一资源所产生的问题,但是我们思考一下这样子合理吗?不合理,这会产生另一种问题——线程饥饿。

二、线程饥饿

       那么线程饥饿是什么呢?为了便于理解,我们可以极端地考虑问题,假设在多线程情况下,存在着两类优先级不同的线程,一类线程的优先级非常高,另一类的线程的优先级非常低,他们开始同时争抢临界资源,假设高优先级的线程拿到了资源,上了锁之后,其他的线程只能等。直到该线程使用完临近资源后解锁,接着所有线程又开始争抢资源,而高优先级的线程因为其优先性会再一次争抢到资源,如循环往复,导那些低优先级的线程总是在等待中,永远拿不到或者很少次数拿到资源,这样被称为饥饿或者饿死。这种争抢临界资源的方式虽然是没有什么错误,但是总归来说是不合理的。

三、线程同步

       在线程只使用互斥的方式去访问临界资源的时候,就可能会出现某些线程饥饿的情况。那么在操作系统中有没有一种机制,在某一时刻既可以只让一个线程去访问临界资源,但是又可以让所有的的线程按照一定的顺序访问资源呢?所有的线程就像排队一样一个个轮流访问资源,当某一线程访问玩临界资源的时候,他就去队尾等待。这样所有的线程的执行流都可以访问到资源,从而杜绝了线程饥饿的问题。  这样的机制叫做——同步,即线程同步,在保证临界资源安全的前提下,让执行流访问临界资源具有一定的顺序性

互斥也是同步的一种,尽管只采用互斥后执行流还是乱序的,但是互斥保证了同一时刻只能有一个线程访问临界资源。但是本篇文章在介绍同步的时候,会将两者分开,即同步不包括互斥。

四、条件变量

 那么同步是怎么实现的呢?同步离不开一个东西——条件变量条件变量是一种可以实现线程同步的机制,通过条件变量,可以实现让线程有序的访问临界资源

条件变量,顾名思义它是一个执行的“条件”,当线程需要访问临界资源时,如果临界资源不满足一定的条件,那就让线程进行等待,如果满足条件,则让线程继续恢复执行的机制。它是一个 pthread_cond_t 结构体类型的变量,并且在 pthread 库中也提供了一些条件变量相关的接口


1、cond

cond即 英文单词 condition 的缩写,译为条件。

pthread_cond_t 是定义条件变量的类型。

条件变量的使用是和互斥锁差不多的。

  • 条件变量的初始化可以和互斥量相同有两种,一种是调用接口 pthread_cond_init() 初始化,第一个参数是条件变量的地址,第二个参数是条件变量的属性(暂时不考虑)。需要注意的是,用该接口初始化的条件变量在不需要使用的时候,需要调用 pthread_cond_destroy() 接口来销毁掉。 
  • 使用宏初始化的条件变量就不用手动调用接口来销毁了。

使用条件变量等待的接口: 

  •  这么多等待的接口中 pthread_cond_wait() 接口是最常用的,它是pthread库提供的使用条件变量等待的接口,线程调用此接口,线程就会立即进入等待。
  • pthread_cond_timedwait() 也是pthread提供给的使用条件变量等待的接口,不过看他的名字也知道它是一种定时让线程等待的接口,即可以通过该接口设置一定的时间,在此时间内让线程等待,如果此时间内,条件满足了,线程就会被自动唤醒,继续执行代码
  • 我们可以看到这两个接口的参数中都有 互斥锁 ,他们是和互斥锁一起配合使用的。

上面讲到了两个通过条件变量让线程进行等待的接口,既然有等待的接口,那么自然就存在着通过条件变量去唤醒线程的接口。如下

  • pthread_cond_signal(),调用该接口可以让某个通过指定条件变量陷入等待的线程被唤醒。
  • pthread_cond_broadcast(),调用此接口,可以让通过指定条件变量陷入等待的所有线程被唤醒

2、条件变量的使用

下面我们简单使用一下条件变量,主要看看它是怎么用的。

#include<iostream>
#include<pthread.h>
#include<unistd.h>
using std::cin;
using std::cout;
using std::endl;
pthread_mutex_t mutex=PTHREAD_MUTEX_INITIALIZER;//利用宏初始化全局互斥锁,不用销毁
pthread_cond_t cond;//定义全局条件变量void* Callback(void* argc)
{pthread_detach(pthread_self());//这里让线程自动分离,我们后面不回收它const char* name=(const char*)argc;while(true){pthread_cond_wait(&cond,&mutex);//使用条件变量让进程在这里等待cout<<name<<",tid::"<<pthread_self()<<",running"<<endl;}return nullptr;}
int main()
{pthread_cond_init(&cond,nullptr);//初始化条件变量pthread_t tid1,tid2,tid3;pthread_create(&tid1,nullptr,Callback,(void*)"thread 1");pthread_create(&tid2,nullptr,Callback,(void*)"thread 2");pthread_create(&tid3,nullptr,Callback,(void*)"thread 3");while(true){char c='a';cout<<"Please input your command:(N/Q)::";cin>>c;if(c=='N'|c=='n'){pthread_cond_signal(&cond);//唤醒单个的线程}elsebreak;usleep(1000);//让主线程在这里等待一下防止多线程之间的打印干扰}pthread_cond_destroy(&cond);//销毁条件变量return 0;}

运行结果:

可以看到pthread_cond_signal()对线程的唤醒是以一定顺序来进行的。当然我们也可以使用pthread_cond_broadcast()来广播唤醒所有的在等待中的线程。


上面演示的是cond变量的简单使用,我们在函数中直接让它进行等待,事实上在实际的使用中,当有条件变量不满足时,才会使用条件变量让线程等待。

我们可以设置一个退出条件 quit,为真时即为满足,否则不满足。不满足条件时,就让线程等待,满足条件就唤醒线程。

#include<iostream>
#include<pthread.h>
#include<unistd.h>
using std::cin;
using std::cout;
using std::cerr;
using std::endl;
pthread_mutex_t mutex=PTHREAD_MUTEX_INITIALIZER;
pthread_cond_t cond;
volatile bool quit=false;
void* Callback(void* argc)
{pthread_detach(pthread_self());const char* name=(const char*)argc;while(!quit){pthread_cond_wait(&cond,&mutex);cout<<name<<",tid::"<<pthread_self()<<",running"<<endl;}
//下面释放锁的操作是因为pthread_cond_wait()接口在等待时会释放锁资源,然后被唤醒的时候又会竞争锁资源,如果线程退出条件满足了,在退出的时候,仍然是对临界资源上了锁,所以在退出之前需要先解锁,不然会导致死锁(如果不提前进行分离)pthread_mutex_unlock(&mutex);cout<<name<<",tid::"<<pthread_self()<<",end"<<endl;return nullptr;}
int main()
{pthread_cond_init(&cond,nullptr);pthread_t tid1,tid2,tid3;pthread_create(&tid1,nullptr,Callback,(void*)"thread 1");pthread_create(&tid2,nullptr,Callback,(void*)"thread 2");pthread_create(&tid3,nullptr,Callback,(void*)"thread 3");while(true){char c='a';cout<<"Please input your command:(N/Q)::";cin>>c;if(c=='N'|c=='n'){pthread_cond_broadcast(&cond);}else{quit=true;pthread_cond_broadcast(&cond);break;}usleep(1000);}pthread_cond_destroy(&cond);return 0;}

这里比之前的简单应用主要多了一个解锁操作。且在 输入非N或n时,唤醒线程,再让线程判断一下条件是否满足。

可以看到 使用条件变量可以让多线程的执行具有一定的顺序性,即可以实现同步。同步与互斥是互补的关系。

五、条件变量与互斥锁

在我们上面所举的例子当中,让线程根据条件变量进行等待的接口都是需要同时用到条件变量和互斥锁,使用到条件变量这是无可厚非的,但是为什么需要用到互斥锁呢?

首先,条件等待是使用条件变量实现同步等待的一种方式,如果只存在一个线程的话,当条件不满足时,线程就会一直等待下去,因为唯一的线程在等待,并没有其他的线程修改条件,所以在线程等待的时候,条件也不可能满足。

所以这里需要的是一个使得条件变得满足,然后再唤醒等待的线程。这里的条件实际上就是指 线程对应的需要访问的临界资源的状态,就像我们在介绍互斥时的抢票动作,需要保证只有在票数大于0时,才能抢票。

而条件是不可能无缘无故在没有变化的情况下就自己满足的,所以条件满足势必会存在着临界资源数据的变化,所以需要用互斥锁来保护临界资源。

所以线程在判断条件满足之前需要先上锁,然后再判断条件是否满足,如果不满足则条件等待并解锁,接着让其他可以让条件满足的线程获取锁,条件满足之后,再唤醒刚才等待的线程并解锁。让刚被唤醒的线程再次取到锁,判断条件是否满足,满足就去执行,否则再次陷入等待。整个过程的重点就是谁需要访问临界资源就上锁,谁不需要就解锁,即保证在整个的过程当中临界资源始终是被保护着的。

整个的过程当中,除了第一次对临界资源上锁和最后一次对临界资源解锁,中间所有的上锁和解锁操作都是由pthread_cond_wait()操作完成的,在线程需要等待的时候调用pthread_cond_wait()解锁并等待,在线程被唤醒时,会自动再去竞争锁,解锁和上锁操作都是在pthread_cond_wait()内部进行的。这就是为什么我们在上面的例子中在多线程退出时,需要在条件满足时先释放锁,然后再让线程退出。

pthread_cond_wait()接口需要执行释放锁和竞争锁的操作,所以需要先看到锁这也是为什么该接口需要和互斥锁一起使用。


http://www.ppmy.cn/devtools/166047.html

相关文章

16 HarmonyOS NEXT UVList组件开发指南(三)

温馨提示&#xff1a;本篇博客的详细代码已发布到 git : https://gitcode.com/nutpi/HarmonyosNext 可以下载运行哦&#xff01; HarmonyOS NEXT UVList组件开发指南(三) 第三篇&#xff1a;UVList组件使用方法与实际应用 1. 基础使用方法 1.1 引入组件 使用UVList组件前&a…

代码随想录第五十天| 图论理论基础

图论理论基础 这篇我们将正式开始学习图论&#xff01; 在代码随想录中&#xff0c;图论相关的算法题目将统一使用ACM模式。为什么要使用ACM模式呢&#xff1f; 图的基本概念 在二维坐标中&#xff0c;两点可以连成线&#xff0c;多个点连成的线就构成了图。 当然&#xff0…

【数据可视化之点位分布图】用图标、颜色、大小讲好数据故事!

信息爆炸的时代&#xff0c;数据可视化的核心不再是简单的图表堆砌&#xff0c;而是如何用视觉语言讲好一个故事。点位分布图作为地理数据的“空间叙事者”&#xff0c;以直观的表达和丰富的视觉信息&#xff0c;成为城市规划、环境监测、交通管理、水利水务等领域的决策利器。…

java中实体类常见的设计模式

实体类常见的设计模式 1. Set 链式编程 在实体类中实现链式调用通常是指让 setter 方法返回当前对象实例&#xff08;this&#xff09;&#xff0c;从而允许连续调用多个 setter 方法设置属性值。这种方式可以使代码更加简洁和直观。 例如实体类为&#xff1a; public clas…

【C++设计模式】第八篇:组合模式(Composite)

注意&#xff1a;复现代码时&#xff0c;确保 VS2022 使用 C17/20 标准以支持现代特性。 树形结构的统一操作接口 1. 模式定义与用途 ​ 核心思想 ​组合模式&#xff1a;将对象组合成树形结构以表示“部分-整体”层次结构&#xff0c;使得客户端可以统一处理单个对象和组合…

《OpenCV》——光流估计

什么是光流估计&#xff1f; 光流估计的前提&#xff1f; 基本假设 亮度恒定假设&#xff1a;目标像素点的亮度在相邻帧之间保持不变。这是光流计算的基础假设&#xff0c;基于此可以建立数学方程来求解光流。时间连续或运动平滑假设&#xff1a;相邻帧之间的时间间隔足够小&a…

大语言模型在患者交互任务中的临床使用评估框架

An evaluation framework for clinical use of large language models in patient interaction tasks An evaluation framework for clinical use of large language models in patient interaction tasks | Nature Medicine 2025.1 收到时间&#xff1a;2023 年 8 月 8 日 …

基于AT89C51单片机的家用全自动洗衣机设计

点击链接获取Keil源码与Project Backups仿真图&#xff1a; https://download.csdn.net/download/qq_64505944/90468876?spm1001.2014.3001.5503 C19 部分参考设计如下&#xff1a; 摘 要 全自动洗衣机的控制系统基于单片机设计&#xff0c;具有高度自动化、智能化的特点。…