Linux系统编程--线程同步

news/2025/3/11 6:05:16/

目录

一、前言

二、线程饥饿

三、线程同步

四、条件变量

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/news/1578268.html

相关文章

前端知识点---前端里的接口

文章目录 1. 接口&#xff08;Interface&#xff09;作为对象的类型定义&#xff1a;① 接口是对象的模版, 类也是对象的模版 可以定义对象的属性跟类型1. 接口是对象的模板&#xff1a;2. 类是对象的模板 ②类可以被具体实现 可以new一个类, 接口只能用来定义类型 接口没法被具…

SQLiteStudio:一款免费跨平台的SQLite管理工具

SQLiteStudio 是一款专门用于管理和操作 SQLite 数据库的免费工具。它提供直观的图形化界面&#xff0c;简化了数据库的创建、编辑、查询和维护&#xff0c;适合数据库开发者和数据分析师使用。 功能特性 SQLiteStudio 提供的主要功能包括&#xff1a; 免费开源&#xff0c;可…

解决Node Electron下调用Python脚本输出中文乱码的问题

博主原博客地址&#xff1a;https://www.lisok.cn/Front-End/610.html 调用Pyinstaller打包后的可执行文件方式如下: import { promisify } from util import { exec } from child_process import { app } from electronasync handleVerifyZy(id) {const entity await this.f…

单片机项目复刻需要的准备工作

一、前言 复刻单片机的项目的时候&#xff0c;有些模块是需要焊接的。很多同学对焊接没有概念。 这里说一下做项目的基本工具。 比如&#xff1a;像这种模块&#xff0c;都需要自己焊接了排针才可以链接的。 二、基本模块 2.1 单排排针 一些模块买回来是没有焊接的&#x…

55. 跳跃游戏(力扣)

给你一个非负整数数组 nums &#xff0c;你最初位于数组的 第一个下标 。数组中的每个元素代表你在该位置可以跳跃的最大长度。 判断你是否能够到达最后一个下标&#xff0c;如果可以&#xff0c;返回 true &#xff1b;否则&#xff0c;返回 false 。 示例 1&#xff1a; 输…

若依前后端分离版使用Electron打包前端Vue为Exe文件

1.前言 本文详细介绍如何使用electron将若依框架前后端分离版的前端Vue页面打包为Exe文件&#xff0c;并且包括如何实现应用更新。使用若依基础代码体现不出打包功能&#xff0c;因此我使用开发的文件管理系统&#xff0c;介绍上述过程&#xff0c;具体可以查看我的文章《若依…

Java Spring MVC (2)

常见的Request Controller 和 Response Controller 的区别 用餐厅点餐来理解 想象你去一家餐厅吃饭&#xff1a; Request Controller&#xff08;接单员&#xff09;&#xff1a;负责处理你的点餐请求&#xff0c;记录你的口味、桌号等信息。Response Controller&#xff08…

【linux网络编程】端口

一、端口&#xff08;Port&#xff09;概述 在计算机网络中&#xff0c;端口&#xff08;Port&#xff09; 是用来标识不同进程或服务的逻辑通信端点。它类似于一座大楼的房间号&#xff0c;帮助操作系统和网络协议区分不同的应用程序&#xff0c;以便正确地传输数据。 1. 端口…