【Linux--多线程同步与互斥】

目录

  • 一、线程互斥
    • 1.1相关概念介绍
    • 1.2互斥量mutex
    • 1.3互斥量接口
      • 1.3.1初始化互斥量
      • 1.3.2销毁互斥量
      • 1.3.3互斥量加锁
      • 1.3.4互斥量解锁
      • 1.3.5使用互斥量解决上面分苹果问题
    • 1.4互斥原理
  • 二、可重入与线程安全
    • 2.1相关概念
    • 2.2常见线程不安全的情况
    • 2.3常见不可重入的情况
    • 2.4 可重入与线程安全的关系
  • 三、死锁
  • 四、线程同步
  • 4.1同步概念与竞态条件
  • 4.2条件变量
    • 4.2.1概念
    • 4.2.2接口
      • 4.2.2.1初始化条件变量
      • 4.2.2.2销毁条件变量
      • 4.2.2.2等待条件变量满足
      • 4.2.2.3唤醒等待
      • 4.2.2.5改进分苹果

一、线程互斥

1.1相关概念介绍

  • 临界资源: 多线程执行流共享的资源叫做临界资源
  • 临界区: 每个线程内部访问临界资源的代码,被称为临界区
  • 互斥: 任何时刻,互斥保证有且只有一个执行流进入临界区访问临界资源,通常对临界资源起保护作用
  • 原子性: 不会被任何调度机制打断的操作,该操作只有两态:要么完成,要么未完成
    为什么要有线程互斥?下面模拟下4人分苹果的代码。
    代码:
#include<iostream>
using namespace std;
#include<pthread.h>
#include<string>
#include<vector>
#include<unistd.h>
const int NUM=4;
class ThreadDate
{
public: ThreadDate(string name){_name=name;}string _name;
};
int Apples=100;
void* GetApple(void* args)
{ThreadDate* td=static_cast<ThreadDate*>(args);while(1){if(Apples>0){sleep(1);cout<<td->_name<<"get a apple,apple number"<<--Apples<<endl;}else break;}return nullptr;
}
int main()
{vector<pthread_t> tids;vector<ThreadDate*> tds;for(int i=0;i<NUM;i++){string name="thread"+to_string(i);ThreadDate* td=new ThreadDate(name);tds.push_back(td);pthread_t tid;pthread_create(&tid,nullptr,GetApple,tds[i]);tids.push_back(tid);}for(int i=0;i<NUM;i++){pthread_join(tids[i],nullptr);delete tds[i];}return 0;
}

现象:
在这里插入图片描述
产生该现象的原因:
在这里插入图片描述

1.2互斥量mutex

若线程使用的数据是局部变量,变量的地址空间在线程栈空间内,变量归属单个线程,其他线程无法获得这种变量;但有些变量需要在线程间共享(共享变量),可以通过数据的共享,完成线程之间的交互。多个线程并发的操作共享变量就会带来一些问题

要解决上述分苹果的问题,需要做到三点:

代码必须有互斥行为:当代码进入临界区执行时,不允许其他线程进入该临界区。
若多个线程同时要求执行临界区的代码,并且此时临界区没有线程在执行,那么只能允许一个线程进入该临界区
若线程不在临界区中执行,那么该线程不能阻止其他线程进入临界区
这时就需要一把锁,Linux中提供的这把锁被称为互斥量
在这里插入图片描述

1.3互斥量接口

1.3.1初始化互斥量

int pthread_mutex_init(pthread_mutex_t *restrict mutex, const pthread_mutexattr_t *restrict attr);

参数:

  • mutex:需要初始化的互斥量的地址
  • attr:初始化互斥量的属性,一般设置为nullptr即可
    返回值:
  • 互斥量初始化成功返回0,失败返回错误码
    使用pthread_mutex_init()函数初始化互斥量的方式被称为动态分配,还可以使用静态分配进行初始化,即下面这种方式:
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;

1.3.2销毁互斥量

int pthread_mutex_destroy(pthread_mutex_t *mutex);

参数mutex:需要销毁的互斥量的地址

返回值:互斥量销毁成功返回0,失败返回错误码

注意:

  • 使用PTHREAD_MUTEX_INITIALIZER初始化的互斥量不需要销毁
  • 不要销毁一个已经加锁的互斥量
  • 已经销毁的互斥量,要确保后面不会有线程再尝试加锁

1.3.3互斥量加锁

int pthread_mutex_lock(pthread_mutex_t *mutex);

参数mutex:需要加锁的互斥量的地址

返回值:互斥量加锁成功返回0,失败返回错误码

注意:

  • 互斥量处于未锁状态时,该函数会将互斥量锁定,同时返回成功
  • 发起函数调用时,若其他线程已经锁定互斥量,或者存在其他线程同时申请互斥量,但没有竞争到互斥量,那么线程会在pthread_mutex_lock()函数内部阻塞至互斥量解锁

1.3.4互斥量解锁

int pthread_mutex_unlock(pthread_mutex_t *mutex);

参数mutex:需要解锁的互斥量的地址

返回值:互斥量解锁成功返回0,失败返回错误码

1.3.5使用互斥量解决上面分苹果问题

#include<iostream>
using namespace std;
#include<pthread.h>
#include<string>
#include<vector>
#include<unistd.h>
const int NUM=4;
class ThreadDate
{
public: ThreadDate(string name,pthread_mutex_t* lock){_name=name;_lock=lock;}
public:string _name;pthread_mutex_t* _lock;};
int Apples=100;
void* GetApple(void* args)
{ThreadDate* td=static_cast<ThreadDate*>(args);while(1){pthread_mutex_lock(td->_lock);if(Apples>0){//sleep(1);cout<<td->_name<<"get a apple,apple number"<<Apples--<<endl;pthread_mutex_unlock(td->_lock);}else {pthread_mutex_unlock(td->_lock);break;}}return nullptr;
}
int main()
{pthread_mutex_t lock;pthread_mutex_init(&lock,nullptr);vector<pthread_t> tids;vector<ThreadDate*> tds;for(int i=0;i<NUM;i++){string name="thread"+to_string(i);ThreadDate* td=new ThreadDate(name,&lock);tds.push_back(td);pthread_t tid;pthread_create(&tid,nullptr,GetApple,tds[i]);tids.push_back(tid);}for(int i=0;i<NUM;i++){pthread_join(tids[i],nullptr);delete tds[i];}pthread_mutex_destroy(&lock);return 0;
}

写这个代码的时候出现了一个乌龙。写到这里复盘以下,顺便提一嘴,多线程写代码时考虑的是要多一点。
在这里插入图片描述

1.4互斥原理

引入互斥量后,当一个线程申请到锁进入临界区时,在其他线程看来该线程只有两种状态,要么没有申请锁,要么锁已经释放了,因为只有这两种状态对其他线程才是有意义的。

例如,图中线程1进入临界区后,在线程2、3、4看来,线程1要么没有申请锁,要么线程1已经将锁释放了,因为只有这两种状态对线程2、3、4才是有意义的,当线程2、3、4检测到其他状态(线程1持有锁)时也就被阻塞了。此时对于线程2、3、4而言,线程1的整个操作过程是原子的
在这里插入图片描述
临界区内的线程可能被切换吗?
临界区内的线程是可能进行线程切换。但即便该线程被切走,其他线程也无法进入临界区进行资源访问,因为此时该线程是拿着锁被切走的,锁没有被释放也就意味着其他线程无法申请到锁,也就无法进入临界区进行资源访问了。
互斥锁是否需要被保护?
既然锁是临界资源,那么锁就必须被保护起来,但锁本身就是用来保护临界资源的,那锁又由谁来保护的呢?

锁实际上是自己保护自己的,只需要保证申请锁的过程是原子的,那么锁就是安全的
如何保证申请锁是原子的?
为了实现互斥锁操作,大多数体系结构都提供了swap或exchange指令,该指令的作用就是把寄存器和内存单元的数据相交换。由于只有一条指令,保证了原子性。
lock和unlock的伪代码:
d6160.png)
6c7540cf84ddb60b3d828201.png)

二、可重入与线程安全

2.1相关概念

  • 线程安全: 多个线程并发同一段代码时,不会出现不同的结果
  • 重入:同一个函数被不同的执行流调用,当前一个流程还没有执行完,就有其他的执行流再次进入,称之为重入。一个函数在重入的情况下,运行结果不会出现任何不同或者任何问题,则该函数被称为可重入函数,否则是不可重入函数。

2.2常见线程不安全的情况

  • 不保护共享变量的函数
  • 函数状态随着被调用,状态发生变化的函数
  • 返回指向静态变量指针的函数
  • 调用线程不安全函数的函数

2.3常见不可重入的情况

  • 调用了malloc/free函数,因为malloc函数是用全局链表来管理堆的
  • 调用了标准I/O库函数,标准I/O可以的很多实现都是以不可重入的方式使用全局数据结构
  • 函数体内使用了静态的数据结构

2.4 可重入与线程安全的关系

  • 线程安全不一定是可重入的,而可重入函数则一定是线程安全的(可重入函数是线程安全函数的一种)
  • 函数是不可重入的,那就不能由多个线程使用,有可能引发线程安全问题
  • 若一个函数中有全局变量,那么这个函数既不是线程安全也不是可重入的
  • 若对临界资源的访问加上锁,则这个函数是线程安全的,但如果这个重入函数的锁还未释放则会产生死锁,因此是不可重入的

三、死锁

死锁是指在一组进程中的各个进程均占有不会释放的资源,但因互相申请被其他进程所占用不会释放的资源而处于的一种永久等待状态
一个锁锁死
在这里插入图片描述
多个锁锁死
线程A申请锁资源的顺序为:锁1、锁2;线程B申请锁资源的顺序为:锁2、锁1

当线程A申请到锁1准备申请锁2时,线程B已申请到锁2准备申请锁1,这时两个线程都会因为申请锁失败而陷入阻塞,并且无法释放锁,进入死锁状态
产生死锁的条件:

  • 互斥条件: 一个资源每次只能被一个执行流使用
  • 请求与保持条件: 一个执行流因请求资源而阻塞时,对已获得的资源保持不放
  • 不剥夺条件: 一个执行流已获得的资源,在未使用完之前,不能强行剥夺
  • 循环等待条件: 若干执行流之间形成一种头尾相接的循环等待资源的关系
    避免死锁
  • 破坏死锁的四个必要条件
  • 加锁顺序一致
  • 避免锁未释放的场景
  • 资源一次性分配

四、线程同步

4.1同步概念与竞态条件

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

  • 单纯的加锁是会存在某些问题的,若某个线程的优先级较高或竞争力较强,每次都能够申请到锁,但申请到锁之后什么也不做,那么这个线程就一直在申请锁和释放锁,这就可能导致其他线程长时间竞争不到锁,引起饥饿问题
  • 单纯的加锁是没有错的,它能够保证在同一时间只有一个线程进入临界区,但它没有高效的让每一个线程使用这份临界资源
  • 现在增加一个规则,当一个线程释放锁后,这个线程不能立马再次申请锁,该线程必须排到这个锁的资源等待队列的最后
  • 增加这个规则之后,下一个获取到锁的资源的线程就一定是在资源等待队列首部的线程,若有十个线程,就能够让这十个线程按照某种次序进行临界资源的访问

4.2条件变量

4.2.1概念

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

条件变量主要包括两个动作:

  • 一个线程等待条件变量的条件成立而被挂起
  • 另一个线程使条件成立后唤醒等待的线程

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

4.2.2接口

4.2.2.1初始化条件变量

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;

4.2.2.2销毁条件变量

int pthread_cond_destroy(pthread_cond_t *cond);

参数cond:需要销毁的条件变量的地址

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

注意:使用PTHREAD_COND_INITIALIZER初始化的条件变量不需要销毁

4.2.2.2等待条件变量满足

int pthread_cond_wait(pthread_cond_t *restrict cond, pthread_mutex_t *restrict mutex);

参数:

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

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

4.2.2.3唤醒等待

int pthread_cond_broadcast(pthread_cond_t *cond);
int pthread_cond_signal(pthread_cond_t *cond);
  • pthread_cond_signal()函数用于唤醒该条件变量等待队列中首个线程
  • pthread_cond_broadcast()函数用于唤醒该条件变量等待队列中的全部线程

参数cond:唤醒在cond条件变量下等待的线程

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

4.2.2.5改进分苹果

#include<iostream>
using namespace std;
#include<pthread.h>
#include<string>
#include<vector>
#include<unistd.h>
const int NUM=4;pthread_cond_t cond=PTHREAD_COND_INITIALIZER;class ThreadDate
{
public: ThreadDate(string name,pthread_mutex_t* lock){_name=name;_lock=lock;}
public:string _name;pthread_mutex_t* _lock;};
int Apples=10;
int flag=NUM;
void* GetApple(void* args)
{ThreadDate* td=static_cast<ThreadDate*>(args);while(1){pthread_mutex_lock(td->_lock);pthread_cond_wait(&cond,td->_lock);//线程在等待队列的时候会自动释放锁if(Apples>0){cout<<td->_name<<"get a apple,apple number"<<Apples--<<endl;pthread_mutex_unlock(td->_lock);}else {pthread_mutex_unlock(td->_lock);break;}}cout<<td->_name<<" "<<"quit!"<<endl;pthread_mutex_lock(td->_lock);pthread_cond_wait(&cond,td->_lock);flag--;pthread_mutex_unlock(td->_lock);return nullptr;
}
int main()
{pthread_mutex_t lock;pthread_mutex_init(&lock,nullptr);vector<pthread_t> tids;vector<ThreadDate*> tds;for(int i=0;i<NUM;i++){string name="thread"+to_string(i);ThreadDate* td=new ThreadDate(name,&lock);tds.push_back(td);pthread_t tid;pthread_create(&tid,nullptr,GetApple,tds[i]);tids.push_back(tid);}sleep(3);while(flag){pthread_cond_signal(&cond);sleep(1);}for(int i=0;i<NUM;i++){pthread_join(tids[i],nullptr);delete tds[i];}pthread_mutex_destroy(&lock);pthread_cond_destroy(&cond);return 0;
}

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

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

相关文章

Python+Django 构建实验室药品管理和预警系统【源码】

人生苦短&#xff0c;我用 Python。 今天给大家分享一个完整的实战案例&#xff1a;Python实现实验室药品管理和预警系统&#xff0c;文末附完整代码! 在线演示环境 项目演示地址&#xff1a;http://101.34.18.118:8002/ &#xff08;图片未压缩&#xff0c;所以加载有点慢&…

穷举vs暴搜vs深搜vs回溯vs剪枝

欢迎来到Cefler的博客&#x1f601; &#x1f54c;博客主页&#xff1a;那个传说中的man的主页 &#x1f3e0;个人专栏&#xff1a;题目解析 &#x1f30e;推荐文章&#xff1a;题目大解析&#xff08;3&#xff09; 目录 &#x1f449;&#x1f3fb;全排列&#x1f449;&#…

浅析锂电池保护板(BMS)系统设计思路(四)SOC算法-扩展Kalman滤波算法

1 SOC估算方法介绍 电池SOC的估算是电池管理系统的核心&#xff0c;自从动力电池出现以来&#xff0c;各种各样的电池SOC估算方法不断出现。随着电池管理系统的逐渐升级&#xff0c;电池SOC估算方法的效率与精度不断提高&#xff0c;下面将介绍常用几种电池SOC估算方法[1]&…

unknown variable ‘authentication_policy=mysql_native_password‘

unknown variable authentication_policymysql_native_password 背景解决尝试一尝试二(解决) 总结 背景 mac上安装多个版本数据库。我是通过dmg安装的&#xff0c;先装的5.7&#xff0c;再装的5.8&#xff0c;然后5.8的能正常用&#xff0c;5.7的启动不起来。报错信息为如下 …

Django 学习教程- Django模板(Template)

系列 Django 学习教程-介绍与安装-CSDN博客 Django 学习教程- Hello world入门案例-CSDN博客 前言 在上一章节中我们使用django.http.HttpResponse() 来输出 "Hello World&#xff01;"。该方式将数据与视图混合在一起&#xff0c;不符合 Django 的 MTV 思想。 本…

node 项目中 __dirname / __filename 是什么,为什么有时候不能用?

__dirname 是 Node.js 中的一个特殊变量&#xff0c;表示当前执行脚本所在的目录的绝对路径。 __filename 同理&#xff0c;是 Node.js 中的一个特殊变量&#xff0c;表示当前执行脚本的绝对路径&#xff0c;包括文件名。 在 Node.js 中&#xff0c;__dirname / __filename是…

Primavera Unifier 项目控制延伸:Phase Gate理论:2/3

阶段Gate的具体内容&#xff1a; 阶段0 根据公司需要和资源现状&#xff0c;决定开展哪些项目。在这个阶段&#xff0c;公司一般需要开展一些脑力风暴或者团队集思广益的活动以获得足够多的点子。一旦团队决定采用某个想法&#xff0c;必须从各个维度去完善它&#xff0c;并使…

Linux服务器搭建笔记-006:拓展/home目录容量

一、问题说明 Ubuntu服务器在使用过程中创建的新用户&#xff0c;每位用户会在/home目录下生成一个属于其个人的主文件夹。如果不限制各个用户的使用空间&#xff0c;所有的用户都会共用/home所挂载的硬盘。在这种多用户情况下&#xff0c;会很快的填满/home目录&#xff0c;导…

B2005 字符三角形(python)

a input() print( a) print( a a a) print(a a a a a)python中默认输入的是字符型&#xff0c;第一句就是输入了一个字符赋给a python中单引号内的也是字符串&#xff0c;用print输出需要连接的字符串时用加号加在后面即可

HarmonyOS应用开发-搭建开发环境

本文介绍如何搭建 HarmonyOS 应用的开发环境&#xff0c;介绍下载安装 DevEco Studio 开发工具和 SDK 的详细流程。华为鸿蒙 DevEco Studio 是面向全场景的一站式集成开发环境&#xff0c;面向全场景多设备&#xff0c;提供一站式的分布式应用开发平台&#xff0c;支持分布式多…

2024年人工智能领域10大预测

2023年人工智能领域如果只能筛选一个关键词的话&#xff0c;恐怕非“大模型”莫属。大模型的发展在过去一年中&#xff0c;让各行各业发生了天翻地覆的变化&#xff0c;有企业因大模型而新生&#xff0c;有企业因大模型而消亡。企业的变迁跟技术迭代息息相关&#xff0c;而大模…

C# MVC +Layui侧边导航栏的收缩及展开

目录 1、头部代码 2、侧边栏&#xff08;例子只写了一级导航&#xff0c;需要多级可自行添加&#xff09; 3、body内容填充 4、 JS 1、头部代码 <div class"layui-layout layui-layout-admin"> <div class"layui-header"> …

27 UVM queue

uvm_queue类构建一个动态队列&#xff0c;该队列将按需分配并通过引用传递。 uvm_queue类声明&#xff1a; class uvm_queue #( type T int ) extends uvm_object 1 uvm_queue class hierarchy 2 uvm_queue class Methods 3 UVM Queue Example 在下面的示例中&#xff0c;…

MySQL 数值函数,字符串函数与多表查询

MySQL像其他语言一样,也提供了很多库函数,分为单行函数和分组函数(聚合函数),我们这里先简易介绍一些函数,熟悉就行,知道怎么使用即可. 数值函数 三角函数 指数与对数函数 进制间的转换函数 字符串函数 注:LPAD函数是右对齐,RPAD函数是左对齐 多表查询 注:如果为表起了别名,就…

easyx的窗口函数

文章目录 前言一、EasyX的颜色二、EasyX的坐标和设备1&#xff0c;EasyX的坐标2&#xff0c;EasyX的设备 三、窗口函数1&#xff0c;初始化窗口函数2&#xff0c;关闭绘图窗口3&#xff0c;设置窗口背景板颜色4&#xff0c;清空绘图设备 前言 easyx是针对c的图形库&#xff0c;…

[ffmpeg系列 02] 音视频基本知识

一 视频 RGB&#xff1a; AV_PIX_FMT_RGB24, ///< packed RGB 8:8:8, 24bpp, RGBRGB… Y&#xff1a;明亮度, Luminance或luma, 灰阶图&#xff0c; UV&#xff1a;色度&#xff0c;Chrominance或Chroma。 YCbCr: Cb蓝色分量&#xff0c;Cr是红色分量。 取值范围&#xff…

判断电话号码是否重复-excel

有时候重复的数据不需要或者很烦人&#xff0c;就需要采取措施&#xff0c;希望以下的方法能帮到你。 1.判断是否重复 方法一&#xff1a; 1&#xff09;针对第一个单元格输入等号&#xff0c;以及公式countif(查找记录数的范围&#xff0c;需要查找的单元格&#xff09; 2…

网络编程『简易TCP网络程序』

&#x1f52d;个人主页&#xff1a; 北 海 &#x1f6dc;所属专栏&#xff1a; Linux学习之旅、神奇的网络世界 &#x1f4bb;操作环境&#xff1a; CentOS 7.6 阿里云远程服务器 文章目录 &#x1f324;️前言&#x1f326;️正文TCP网络程序1.字符串回响1.1.核心功能1.2.程序…

AIGC开发:调用openai的API接口实现简单机器人

简介 开始进行最简单的使用&#xff1a;通过API调用openai的模型能力 OpenAI的能力如下图&#xff1a; 文本生成模型 OpenAI 的文本生成模型&#xff08;通常称为生成式预训练 Transformer 或大型语言模型&#xff09;经过训练可以理解自然语言、代码和图像。这些模型提供文…

logstash收集华为、H3C、Cisco交换机日志

网络设备配置 将 syslog-ip 替换成服务器的IP地址。 Huawei info-center loghost source interface info-center loghost syslog-ip local-time facility local6 H3C info-center loghost source interface info-center loghost syslog-ip facility local5 Aruba logging arm …