【Linux】多线程4——线程同步/条件变量

1.Linux线程同步

1.1.同步概念与线程饥饿问题

先来理解同步的概念

  • 什么是线程同步

        在一般情况下,创建一个线程是不能提高程序的执行效率的,所以要创建多个线程。但是多个线程同时运行的时候可能调用线程函数,在多个线程同时对同一个内存地址进行写入,由于CPU时间调度上的问题,写入数据会被多次的覆盖,所以就要使线程同步。

同步就是协同步调,按预定的先后次序进行运行。如:你说完,我再说。

“同”字从字面上容易理解为一起动作,但其实不是,“同”字应是指协同、协助、互相配合。

        如进程、线程同步,可理解为进程或线程A和B一块配合,A执行到一定程度时要依靠B的某个结果,于是停下来,示意B运行;B依言执行,再将结果给A;A再继续操作。

        所谓同步,就是在发出一个功能调用时,在没有得到结果之前,该调用就不返回,同时其它线程也不能调用这个方法。按照这个定义,其实绝大多数函数都是同步调用(例如sin, isdigit等)。但是一般而言,我们在说同步、异步的时候,特指那些需要其他部件协作或者需要一定时间完成的任务。例如Window API函数SendMessage。该函数发送一个消息给某个窗口,在对方处理完消息之前,这个函数不返回。当对方处理完毕以后,该函数才把消息处理函数所返回的LRESULT值返回给调用者。

  • 同步: 在保证数据安全的前提下,让线程能够按照某种特定的顺序访问临界资源,从而有效避免饥饿问题,这就叫做同步。
  • 竞态条件: 因为时序问题,而导致程序异常,我们称之为竞态条件。
  • 线程饥饿问题

        首先需要明确的是,单纯的加锁是会存在某些问题的,如果个别线程的竞争力特别强,每次都能够申请到锁,但申请到锁之后什么也不做,所以在我们看来这个线程就一直在申请锁和释放锁,这就可能导致其他线程长时间竞争不到锁,引起饥饿问题。

        单纯的加锁是没有错的,它能够保证在同一时间只有一个线程进入临界区,但它没有高效的让每一个线程使用这份临界资源。

        现在我们增加一个规则,当一个线程释放锁后,这个线程不能立马再次申请锁,该线程必须排到这个锁的资源等待队列的最后。

        增加这个规则之后,下一个获取到锁的资源的线程就一定是在资源等待队列首部的线程,如果有十个线程,此时我们就能够让这十个线程按照某种次序进行临界资源的访问。

        例如,现在有两个线程访问一块临界区,一个线程往临界区写入数据,另一个线程从临界区读取数据,但负责数据写入的线程的竞争力特别强,该线程每次都能竞争到锁,那么此时该线程就一直在执行写入操作,直到临界区被写满,此后该线程就一直在进行申请锁和释放锁。而负责数据读取的线程由于竞争力太弱,每次都申请不到锁,因此无法进行数据的读取,引入同步后该问题就能很好的解决。

1.2.条件变量

我们怎么实现线程同步呢?这需要学习Linux的条件变量。

  • 什么是条件变量?该不会真就是1个变量吧!!!

千万不要被误导了,条件变量可不是变量, 条件变量是利用线程间共享的全局变量进行同步的一种机制,条件变量是用来描述某种资源是否就绪的一种数据化描述。

条件变量通常需要配合互斥锁一起使用。

        互斥量可以防止多个线程同时访问临界资源,而条件变量允许一个线程将某个临界资源的状态变化通知其他线程,在共享资源设定一个条件变量,如果共享资源条件不满足,则让线程到该条件变量下阻塞等待,当条件满足时,其他线程可以唤醒条件变量阻塞等待的线程。

        在线程之间有一种情况:线程A需要某个条件才能继续往下执行,如果该条件不成立,此时线程A进行阻塞等待,当线程B运行后使该条件成立后,则唤醒该线程A继续往下执行。

        在pthread库中,可以通过条件变量中,可以设定一个阻塞等待的条件,或者唤醒等待条件的线程。

        条件变量是利用线程间共享的全局变量进行同步的一种机制,条件变量是用来描述某种资源是否就绪的一种数据化描述。

条件变量是一种等待机制,每一个条件变量对应一个等待原因与等待队列。一般对于条件变量会有两种操作:

每个条件变量都有属于自己的一个等待队列

  1. wait操作 : 将自己阻塞在等待队列里,唤醒一个等待者或者开放锁的互斥访问
  2. singal 操作 : 唤醒一个等待的线程(等待队列为空的话什么也不做)

1.3.条件变量函数

1.3.1.初始化条件变量

POSIX提供了两种初始化条件变量的方法。

  • 第一种方法

初始化条件变量的函数叫做pthread_cond_init,该函数的函数原型如下:

int pthread_cond_init(pthread_cond_t *restrict cond, const pthread_condattr_t *restrict attr);

参数说明:

  • cond:需要初始化的条件变量。
  • attr:初始化条件变量的属性,一般设置为NULL即可。

返回值说明:

  • 条件变量初始化成功返回0,失败返回错误码。
  • 第二种方法

调用pthread_cond_init函数初始化条件变量叫做动态分配,除此之外,我们还可以用下面这种方式初始化条件变量,该方式叫做静态分配:

pthread_cond_t cond = PTHREAD_COND_INITIALIZER;

这个相当于调用函数pthread_cond_init()初始化,并且参数attr为NULL。 

1.3.2.销毁条件变量

销毁条件变量的函数叫做pthread_cond_destroy,该函数的函数原型如下:

int pthread_cond_destroy(pthread_cond_t *cond);

参数说明:

  • cond:需要销毁的条件变量。

返回值说明:

  • 条件变量销毁成功返回0,失败返回错误码。

销毁条件变量需要注意:

  • 使用PTHREAD_COND_INITIALIZER初始化的条件变量不需要销毁。

1.3.3.等待条件变量满足

        条件变量就是为了与某个条件关联起来使用的,如果条件不满足,就等待(pthread_cond_wait) ,或者等待一段有限的时间(pthread_cond_timedwait) 。

POSIX提供了如下条件变量的等待接口:

        函数描述:这两个函数都是让指定的条件变量进入等待状态,其工作机制是先解锁传入的互斥量,再让条件变量等待,从而使所在线程处于阻塞状态。这两个函数返回时,系统会确保该线程再次持有互斥量(加锁)。

两个函数的区别:

  1. pthread_cond_wait函数调用成功后,会一直阻塞等待,直到条件变量被唤醒。
  2. 而 pthread_cond_timedwait 函数只会等待指定的时间,时间到了之后,条件变量仍未被唤醒的话,会返回一个错误码ETIMEDOUT,该错误码定义在<errno.h>头文件。

参数说明:

  • cond:需要等待的条件变量。
  • mutex:当前线程所处临界区对应的互斥锁。

返回值说明:

函数调用成功返回0,失败返回错误码。

1.3.4.唤醒等待

上面说完了条件等待,接下来介绍条件变量的唤醒。

调用完条件变量等待函数的线程处于阻塞状态,若要被唤醒,必须是其他线程来唤醒。

唤醒等待的函数有以下两个:

#include <pthread.h>
int pthread_cond_signal(pthread_cond_t *cond);
int pthread_cond_broadcast(pthread_cond_t *cond);

必须在互斥锁的保护下使用相应的条件变量。否则对条件变量的解锁有可能发生在锁定条件变量之前,从而造成死锁。 

        pthread_cond_signal 负责唤醒等待在条件变量上的一个线程,如果有多个线程等待,是唤醒哪一个呢?Linux内核会为每个条件变量维护一个等待队列,调用了 pthread_cond_wait 或 pthread_cond_timedwait 的线程会按照调用时间先后添加到该队列中。pthread_cond_signal会唤醒该队列的第一个。

        如果没有线程被阻塞在条件变量上,那么调用pthread_cond_signal()将没有作用。

        pthread_cond_broadcast,就是同时唤醒等待在条件变量上的所有线程。前面说过,条件等待的两个函数返回时,系统会确保该线程再次持有互斥量(加锁),所有,这里被唤醒的所有线程都会去争夺互斥锁,没抢到的线程会继续等待,拿到锁后同样会从条件等待函数返回。所以,被唤醒的线程第一件事就是再次判断条件是否满足!

        由于pthread_cond_broadcast函数唤醒所有阻塞在某个条件变量上的线程,这些线程被唤醒后将再次竞争相应的互斥锁,所以必须小心使用pthread_cond_broadcast函数。

参数说明:

  • cond:唤醒在cond条件变量下等待的线程。

返回值说明:

  • 函数调用成功返回0,失败返回错误码。

使用示例:

我们先下面这样子的

#include <stdio.h>
#include <pthread.h>
#include<iostream>
#include<unistd.h>
using namespace std;int cnt=0;void* Count(void*args)
{pthread_detach(pthread_self());//分离线程long long number=(long long)args;while(1){cout<<"pthread: "<<number<<endl;sleep(3);}
}int main()
{for(long long i=0;i<5;i++){pthread_t tid;pthread_create(&tid,nullptr,Count,(void*)i);}while(1)sleep(1);
}

特别注意:64位平台下面,int类型是4字节,不能和void*指针类型(8字节)进行相互转换 ,所以这里使用long long

        多个执行流向显示器打印,就是往文件里写入,多线程或多进程往同一个文件写入,这个文件就是一种临界资源,不加保护的话,非常容易出现信息干扰。

#include <stdio.h>
#include <pthread.h>
#include<iostream>
#include<unistd.h>
using namespace std;int cnt=0;//全局变量
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;//互斥锁,不需要初始化和销毁
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;//条件变量,不需要初始化和销毁void* Count(void*args)
{pthread_detach(pthread_self());//分离线程long long number=(long long)args;while(1){pthread_mutex_lock(&mutex);//加锁//先不管临界资源的情况cout<<"pthread: "<<number<<" ,cnt :"<<cnt++<<endl;pthread_mutex_unlock(&mutex);//解锁sleep(1);}
}int main()
{for(long long i=0;i<5;i++){pthread_t tid;pthread_create(&tid,nullptr,Count,(void*)i);}while(1)sleep(1);
}

我们给打印这条语句加了锁,打印出来的结果也自然不会混乱了

好了,我今天想说的主角可不是屏幕,而是我们的++操作

我们接下来用上我们的条件变量

#include <stdio.h>
#include <pthread.h>
#include<iostream>
#include<unistd.h>
using namespace std;int cnt=0;//全局变量
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;//互斥锁,不需要初始化和销毁
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;//条件变量,不需要初始化和销毁void* Count(void*args)
{pthread_detach(pthread_self());//分离线程long long number=(long long)args;cout<<"pthread: "<<number<<" creat success !"<<endl;while(1){pthread_mutex_lock(&mutex);//加锁pthread_cond_wait(&cond,&mutex);//先让自己这个线程去条件变量cond的等待队列  //为什么填在这里?pthread_cond_wait让线程等待的时候会自动释放掉//先不管临界资源的情况cout<<"pthread: "<<number<<" ,cnt :"<<cnt++<<endl;pthread_mutex_unlock(&mutex);//解锁sleep(1);}
}int main()
{for(long long i=0;i<5;i++){pthread_t tid;pthread_create(&tid,nullptr,Count,(void*)i);usleep(1000);}sleep(3);cout<<"main thread ctrl begin:"<<endl;while(1){sleep(1);//每过1秒就唤醒1次pthread_cond_signal(&cond);//唤醒在cond的等待队列的1个线程,默认都是第1个cout<<"signal one thread..."<<endl;}
}

      此时我们会发现唤醒这4个线程时具有明显的顺序性,根本原因是当这若干个线程启动时默认都会在该条件变量下去等待,而我们每次都唤醒的是在当前条件变量下等待的头部线程,当该线程执行完打印操作后会继续排到等待队列的尾部进行wait,所以我们能够看到一个周转的现象。

我们可以唤醒所有线程

#include <stdio.h>
#include <pthread.h>
#include<iostream>
#include<unistd.h>
using namespace std;int cnt=0;//全局变量
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;//互斥锁,不需要初始化和销毁
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;//条件变量,不需要初始化和销毁void* Count(void*args)
{pthread_detach(pthread_self());//分离线程long long number=(long long)args;cout<<"pthread: "<<number<<" creat success !"<<endl;while(1){pthread_mutex_lock(&mutex);//加锁pthread_cond_wait(&cond,&mutex);//先让别的线程去等待队列  //为什么填在这里?pthread_cond_wait让线程等待的时候会自动释放掉//先不管临界资源的情况cout<<"pthread: "<<number<<" ,cnt :"<<cnt++<<endl;pthread_mutex_unlock(&mutex);//解锁sleep(1);}
}int main()
{for(long long i=0;i<5;i++){pthread_t tid;pthread_create(&tid,nullptr,Count,(void*)i);usleep(1000);}sleep(3);cout<<"main thread ctrl begin:"<<endl;while(1){sleep(1);pthread_cond_broadcast(&cond);//唤醒在cond的等待队列的1个线程,默认都是第1个cout<<"signal one thread..."<<endl;}
}

我为什么要让一个线程去休眠?

一定是临界资源没有就绪,没错,临界资源也是有状态的

你怎么知道临界资源是就绪还是不就绪的?你判断出来的!那判断是访问临界资源吗? 是的,必须是的

        我们需要判断临界资源状态,就得访问临界资源,而我们的线程对临界资源是会修改的,这就注定了这个判断一定要在加锁和解锁之间,这样子别的线程就不能修改我们的临界资源,我们的判断结果也会是正确的

也就是必须是下面这种结构

void* Count(void*args)
{while(1){pthread_mutex_lock(&mutex);//加锁pthread_cond_wait(&cond,&mutex);//判断资源情况,pthread_mutex_unlock(&mutex);//解锁}
}

这也是我们为什么需要互斥量的原因 

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/385553.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

云服务器Ubuntu18.04进行Nginx配置

云服务器镜像版本信息&#xff1a;Ubuntu 18.04 server 64bit&#xff0c;本文记录了在改版本镜像上安装Nginx&#xff0c;并介绍了Nginx配置文件目录&#xff0c;便于后面再次有需求时进行复习。 文章目录 Nginx的安装Nginx配置文件分析 Nginx的安装 1.执行下面命令进行安装…

linux 部署flask项目

linux python环境安装: https://blog.csdn.net/weixin_41934979/article/details/140528410 1.创建虚拟环境 python3.12 -m venv .venv 2.激活环境 . .venv/bin/activate 3.安装依赖包(pip3.12 install -r requirements.txt) pip3.12 install -r requirements.txt 4.测试启…

使用git命令行的方式,将本地项目上传到远程仓库

在国内的开发环境中&#xff0c;git的使用是必不可少的。Git 是一款分布式版本控制系统&#xff0c;用于有效管理和追踪文件的变更历史及协作开发。本片文章就来介绍一下怎样使用git命令行的方式&#xff0c;将本地项目上传到远程仓库&#xff0c;虽然现在的IDE中基本都配置了g…

React类组件生命周期与this关键字

类组件生命周期 参考链接 一图胜千言&#xff08;不常用的生命周期函数已隐藏&#xff09; 代码&#xff1a; //CC1.js import { Component } from "react";export default class CC1 extends Component {constructor(props) {super(props);console.log("con…

人工智能算法工程师(高级)课程8-图像分割项目之Mask-RCNN模型的介绍与代码详解

大家好,我是微学AI,今天给大家介绍一下人工智能算法工程师(高级)课程8-图像分割项目之Mask-RCNN模型的介绍与代码详解。Mask R-CNN模型是一种广泛应用于目标检测和实例分割的任务的深度学习框架。本文将详细介绍Mask R-CNN的原理,包括Box Regression、Classification和Mask …

追问试面试系列:开篇

我们不管做任何事情&#xff0c;都是需要个理由&#xff0c;而不是盲目去做。 为什么写这个专栏&#xff1f; 就像我们被面试八股文时&#xff0c;市面上有很多面试八股文&#xff0c;随便一个八股文都是500&#xff0c;甚至1000面试题。诸多面试题&#xff0c;难道我们需要一…

Node Js开发环境的搭建

前言 通过自动化繁琐的设置和配置工作&#xff0c;帮助开发者快速启动新项目。常见的Node脚手架工具包括Yeoman、Express Generator、Create React App等。 一、什么是脚手架 1、什么是脚手架&#xff1f; 脚手架在软件开发中指的是一种自动化工具或脚本&#xff0c;用于快速创…

谷粒商城实战笔记-72-商品服务-API-属性分组-获取分类属性分组

文章目录 一&#xff0c;后端接口开发Controller层修改接口接口测试 二&#xff0c;前端开发 这一节的内容是开发获取分类属性分组的接口。 一&#xff0c;后端接口开发 Controller层修改接口 修改AttrGroupController接口。 RequestMapping("/list/{catelogId}")p…

【算法/训练】:动态规划(线性DP)

一、路径类 1. 字母收集 思路&#xff1a; 1、预处理 对输入的字符矩阵我们按照要求将其转换为数字分数&#xff0c;由于只能往下和往右走&#xff0c;因此走到&#xff08;i&#xff0c;j&#xff09;的位置要就是从&#xff08;i - 1&#xff0c; j&#xff09;往下走&#…

【Go系列】Go的UI框架Fyne

前言 总有人说Go语言是一门后端编程语言。 Go虽然能够很好地处理后端开发&#xff0c;但是者不代表它没有UI库&#xff0c;不能做GUI&#xff0c;我们一起来看看Go怎么来画UI吧。 正文 Go语言由于其简洁的语法、高效的性能和跨平台的编译能力&#xff0c;非常适合用于开发GUI…

鸿蒙应用框架开发【dlopen加载so库并获取Rawfile资源】 NDK

dlopen加载so库并获取Rawfile资源 介绍 本示例中主要介绍在TaskPool子线程中使用dlopen加载so库&#xff0c;以及如何使用Native Rawfile接口操作Rawfile目录和文件。功能包括文件列表遍历、文件打开、搜索、读取和关闭Rawfile。 效果预览 使用说明 应用界面中展示了Rawfil…

2024最新Uniapp的H5网页版添加谷歌授权验证

现在教程不少&#xff0c;但是自从谷歌升级验证之后&#xff0c;以前的老教程就失效了&#xff0c;现在写一个新教程以备不时之需。 由于众所周知的特殊原因&#xff0c;开发的时候一定注意网络环境&#xff0c;如果没有梯子是无法进行开发的哦~ clientID的申请方式我就不再进…

昇思MindSpore 应用学习-DCGAN生成漫画头像-CSDN

日期 心得 昇思MindSpore 应用学习-DCGAN生成漫画头像&#xff08;AI代码学习&#xff09; DCGAN生成漫画头像 在下面的教程中&#xff0c;我们将通过示例代码说明DCGAN网络如何设置网络、优化器、如何计算损失函数以及如何初始化模型权重。在本教程中&#xff0c;使用的动…

数据结构:二叉树(堆)的顺序存储

文章目录 1. 树1.1 树的概念和结构1.2 树的相关术语 2. 二叉树2.1 二叉树的概念和结构2.2 二叉树的特点2.3 特殊的二叉树2.3.1 满二叉树2.3.2 完全二叉树 2.4 二叉树的性质 3. 实现顺序结构二叉树3.1 堆的概念和结构3.2 初始化3.3 销毁3.4 插入数据3.5 向上调整算法3.6 删除数据…

如何查找下载安装安卓APK历史版本?

在安卓设备上&#xff0c;有时候我们可能希望安装某个软件的旧版本&#xff0c;可能是因为新版本不兼容、功能改变不符合需求或是其他原因。 安卓系统并不像iOS那样提供直观的历史版本下载界面。 不过&#xff0c;通过一些第三方市场和网站&#xff0c;我们仍然可以找到并安装…

【LLM】-08-搭建问答系统-语言模型,提问范式与 Token

目录 1、语言模型 1.1、训练过程&#xff1a; 1..2、大型语言模型分类&#xff1a; 1.3、指令微调模型训练过程&#xff1a; 2、Tokens 3、Helper function辅助函数 (提问范式) 4、计算token数量 1、语言模型 大语言模型&#xff08;LLM&#xff09;是通过预测下一个词…

【python】sklearn基础教程及示例

【python】sklearn基础教程及示例 Scikit-learn&#xff08;简称sklearn&#xff09;是一个非常流行的Python机器学习库&#xff0c;提供了许多常用的机器学习算法和工具。以下是一个基础教程的概述&#xff1a; 1. 安装scikit-learn 首先&#xff0c;确保你已经安装了Python和…

搜索引擎项目(四)

SearchEngine 王宇璇/submit - 码云 - 开源中国 (gitee.com) 基于Servlet完成前后端交互 WebServlet("/searcher") public class DocSearcherServlet extends HttpServlet {private static DocSearcher docSearcher new DocSearcher();private ObjectMapper obje…

Kettle下载安装

环境说明 虚拟机&#xff1a;Win7&#xff1b;MySql8.0 主机&#xff1a;Win11&#xff1b;JDK1.8&#xff1b;Kettle 9.4&#xff08;Pentaho Data Integration 9.4&#xff09;&#xff08;下载方式见文末&#xff09; 安装说明 【1】解压后运行Spoon.bat 【2】将jar包 复…