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

news2024/11/19 1:25:39

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.coloradmin.cn/o/1955380.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【文件fd】C++文件操作 | 详解系统调用接口文件操作 | 系统调用接口库函数

目录 1.回顾理解&引出问题 2.C文件操作 3.系统调用文件操作 3.0准备工作 3.1版本1☞open 3.2版本2☞文件权限 3.3版本3☞权限掩码 3.4版本3☞标记位传参 3.5版本4☞close 3.6版本5☞write 3.7flags选项 3.7.1 O_WRONLY | O_CREAT 3.7.2 O_WRONLY | O_CREAT …

Servlet详解(超详细)

Servlet详解 文章目录 Servlet详解一、基本概念二、Servlet的使用1、创建Servlet类2、配置Servleta. 使用web.xml配置b. 使用注解配置 3、部署Web应用4、处理HTTP请求和生成响应5、处理表单数据HTML表单Servlet 6、管理会话 三、servlet生命周期1、加载和实例化2、初始化3、 请…

Python爬虫入门02:Fiddler下载使用教程

文章目录 手机抓包全攻略&#xff1a;Fiddler 工具深度解析引言Fiddler 工具简介为什么选择 Fiddler&#xff1f; 安装与配置 Fiddler步骤一&#xff1a;下载与安装步骤二&#xff1a;配置浏览器代理步骤三&#xff1a;安装 HTTPS 证书 配置手机以使用 Fiddler步骤一&#xff1…

鸿蒙应用框架开发【OpenGL三棱椎】 NDK

OpenGL三棱椎 介绍 XComponent控件常用于相机预览流的显示和游戏画面的绘制,在HarmonyOS上&#xff0c;可以配合Native Window创建OpenGL开发环境&#xff0c;并最终将OpenGL绘制的图形显示到XComponent控件。本示例基于"Native C"模板&#xff0c;调用OpenGL(Open…

wpf中团队独立开发模块和左侧2个菜单的框架演示

此篇文章和上一篇文章wpf中开发独立模块功能和左侧1个菜单的框架演示-CSDN博客的结构是一样的&#xff0c;有1点不同的是&#xff0c;左侧有2层菜单&#xff0c;所以&#xff0c;就会更加的复杂。建议&#xff0c;先看明白上一篇的内容&#xff0c;再看这篇文章&#xff0c;否则…

CSS实现表格无限轮播

<div className{styles.tableTh}><div className{styles.thItem} style{{ width: 40% }}>报警名称</div><div className{styles.thItem} style{{ width: 35% }}>开始时间</div><div className{styles.thItem} style{{ width: 25% }}>状态&…

前端三大主流框架对比

在现代前端开发中&#xff0c;React、Vue和Angular是三大流行的框架/库。它们各自有独特的优缺点&#xff0c;适用于不同的开发需求和项目规模。下面是对这三者的详细比较&#xff1a; 一、 React 简介&#xff1a; 由Facebook开发和维护&#xff0c;是一个用于构建用户界面…

亚博科技和幻尔科技的十轴IMU在Ros2 Humble下驱动后数值无限趋于0的解决方案

在做机器人导航以及建模的时候&#xff0c;考虑到多传感器融合可能会带来更好的效果&#xff0c;于是决定使用幻尔科技的十轴IMU&#xff08;其实亚博科技与幻尔科技这块IMU的内部完全一致&#xff0c;驱动代码都完全一致&#xff09;驱动后使用以下命令输出传来的四元数等数据…

自写ApiTools工具,功能参考Postman和ApiPost

近日在使用ApiPost的时候&#xff0c;发现新版本8和7不兼容&#xff0c;也就是说8不支持离线操作&#xff0c;而7可以。 我想说&#xff0c;我就是因为不想登录使用才从Postman换到ApiPost的。 众所周知&#xff0c;postman时国外软件&#xff0c;登录经常性抽风&#xff0c;…

Mike SHE里如何正确设置分区降雨

前言&#xff1a; MIKE SHE分布式水文模型现阶段用于流域洪水的项目比较多&#xff0c;因属于大尺度模型&#xff0c;基本可以模拟水循环全过程&#xff0c;包含降雨—蒸发——产汇流—地表水—地下水等。同时还可以耦合MIKE11水动力水质模型。 今天给大家介绍下MIKESHE是如何…

从零到一使用 Ollama、Dify 和 Docker 构建 Llama 3.1 模型服务

本篇文章聊聊&#xff0c;如何使用 Ollama、Dify 和 Docker 来完成本地 Llama 3.1 模型服务的搭建。 如果你需要将 Ollama 官方不支持的模型运行起来&#xff0c;或者将新版本 llama.cpp 转换的模型运行起来&#xff0c;并且想更轻松的使用 Dify 构建 AI 应用&#xff0c;那么…

进程间的通信(IPC)--管道

1.进程间通信常用的方式 1 &#xff0c;管道通信&#xff1a;有名管道&#xff0c;无名管道 2 &#xff0c;信号 - 系统开销小 3 &#xff0c;消息队列 - 内核的链表 4 &#xff0c;信号量 - 计数器 5 &#xff0c;共享内存 6 &#xff0c;内存映射 7 &#xff0c;套接…

人称“灯爷”的灯光师到底要做些什么,看看他的岗位说明书

灯光师又称“灯爷”,是摄影制作部门负责灯光设备的技术人员,一般归摄影指导调配。被尊称“爷”,可见灯光师的地位不容小觑。那么这个岗位到底要做些什么呢&#xff1f; 岗位职责&#xff1a; 1、负责公司灯光设备的调制、维护和保养&#xff1b; 2、负责各包房灯光设备的调制、…

Mac环境报错 error: symbol(s) not found for architecture x86_64

Mac 环境Qt Creator报错 error: symbol(s) not found for architecture x86_64 错误信息 "symbol(s) not found for architecture x86_64" 通常是在编译或链接过程中出现的问题。这种错误提示通常涉及到符号未找到或者是因为编译器没有找到适当的库文件或函数定义。 …

基于springboot+vue+uniapp的养老院系统小程序

开发语言&#xff1a;Java框架&#xff1a;springbootuniappJDK版本&#xff1a;JDK1.8服务器&#xff1a;tomcat7数据库&#xff1a;mysql 5.7&#xff08;一定要5.7版本&#xff09;数据库工具&#xff1a;Navicat11开发软件&#xff1a;eclipse/myeclipse/ideaMaven包&#…

初识git工具~~上传代码到gitee仓库的方法

目录 1.背景~~其安装 2.gitee介绍 2.1新建仓库 2.2进行相关配置 3.拉取仓库 4.服务器操作 4.1克隆操作 4.2查看本地仓库 4.3代码拖到本地仓库 4.4关于git三板斧介绍 4.4.1add操作 4.4.2commit操作 4.4.3push操作 5.一些其他说明 5.1.ignore说明 5.2git log命令 …

ACC:Automatic ECN Tuning for High-Speed Datacenter Networks 相关知识点介绍(二)

目录 PerfTest工具 Incast traffic Incast Traffic 的原因 Incast Traffic 的影响 解决方法 流量负载 简单解释 影响因素 影响 管理方法 LINKPACK 主要特点 LinkPack 的应用 运行结果 Quantum ESPRESSO 主要特点 TensorFlow 主要特点 主要组件 Incast与qp …

Ubuntu2023.04 浏览器不能上网的问题

1.问题描述 ping www.baidu.com 是可以连接的&#xff0c;但是打开网页就是不能上网&#xff0c;但是自己查看了浏览器上面的设置&#xff0c;代理设置都是关闭的 再看了系统的设置代理也是关闭的&#xff0c;就是上不了网 解决方案&#xff1a; 455 echo $http_proxy456 e…

JavaWeb项目中动态拼接sql语句

业务需求描述&#xff1a; 图中的查询框在分条件查询用户信息列表时&#xff0c;前端可能会传回一个条件或多个条件&#xff0c;此时要对不同的条件进行sql语句的不同书写&#xff0c;前端传的情况有很多种&#xff0c;所以如果分情况写sql语句会比较死&#xff0c;并且不够灵活…

机器学习之人脸识别-使用 scikit-learn 和人工神经网络进行高效人脸识别

文章摘要 本文将介绍如何使用 Python 的 scikit-learn 库和人工神经网络&#xff08;ANN&#xff09;来识别人脸。我们将使用 LFW 数据集&#xff08;Labeled Faces in the Wild&#xff09;&#xff0c;这是一个广泛用于人脸识别基准测试的大型人脸数据库。我们将展示如何准备…