【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://xiahunao.cn/news/2659505.html

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

相关文章

深度解析 | 什么是超融合数据中心网络?

数据中心网络连接数据中心内部通用计算、存储和高性能计算资源&#xff0c;服务器间的所有数据交互都要经由网络转发。当前&#xff0c;IT架构、计算和存储技术都在发生重大变革&#xff0c;驱动数据中心网络从原来的多张网络独立部署向全以太化演进。而传统的以太网无法满足存…

Pycharm引用其他文件夹的py

Pycharm引用其他文件夹的py 方式1&#xff1a;包名设置为Sources ROOT 起包名的时候&#xff0c;需要在该文件夹上&#xff1a;右键 --> Mark Directory as --> Sources ROOT 标记目录为源码目录&#xff0c;就可以了。 再引用就可以了 import common from aoeweb impo…

【C++】开源:cpp-httplib HTTP协议库配置与使用

&#x1f60f;★,:.☆(&#xffe3;▽&#xffe3;)/$:.★ &#x1f60f; 这篇文章主要介绍cpp-httplib HTTP协议库配置与使用。 无专精则不能成&#xff0c;无涉猎则不能通。——梁启超 欢迎来到我的博客&#xff0c;一起学习&#xff0c;共同进步。 喜欢的朋友可以关注一下&a…

美国Top科技公司年薪大曝光,OpenAI 600万高居榜首!

全美顶尖AI公司年薪大曝光&#xff01; OpenAI 600万高居榜首&#xff0c;微软、英伟达只有OpenAI 的一半。 近日&#xff0c;美国一家帮助博士生协商薪资的公司Rora发布了一份薪资报告&#xff0c;公布了这些顶尖AI公司给研究人员开出的平均薪水。 以下是部分顶级AI公司的名…

《PCI Express体系结构导读》随记 —— 第I篇 第1章 PCI总线的基本知识(11)

接前一篇文章&#xff1a;《PCI Express体系结构导读》随记 —— 第I篇 第1章 PCI总线的基本知识&#xff08;10&#xff09; 1.3 PCI总线的存储器读写总线事务 1.3.2 Posted和Non-Posted传送方式 PCI总线规定了两类数据传送方式&#xff0c;分别是Posted和Non-Posted数据传送…

数据仓库 基本信息

数据仓库基本理论 数据仓库&#xff08;英语&#xff1a;Data Warehouse&#xff0c;简称数仓、DW&#xff09;,是一个用于存储、分析、报告的数据系统。数据仓库的目的是构建面向分析的集成化数据环境&#xff0c;为企业提供决策支持&#xff08;Decision Support&#xff09…

【轻松入门】OpenCV4.8 + QT5.x开发环境搭建

引言 大家好&#xff0c;今天给大家分享一下最新版本OpenCV4.8 QT5 如何一起配置&#xff0c;完成环境搭建的。 下载OpenCV4.8并解压缩 软件版本支持 CMake3.13 或者以上版本 https://cmake.org/ VS2017专业版或者以上版本 QT5.15.2 OpenCV4.8源码包 https://github.com/op…

EDKII:第一个Helloworld

目录 0 说明 1 步骤 1.1 简介 1.2 创建新文件 1.3 创建printhelloworld.c、printhelloworld.inf&#xff1a; 1.4 修改MdeModulePkg\MdeModulePkg.dsc 1.5 修改EmulatorPkg\EmulatorPkg.dsc 1.6 运行 0 说明 上篇文章记录了如何安装UEFI环境&#xff0c;在这里将会写下…

启明智显开源项目分享|基于Model 3c芯片的86中控面板ZX3D95CM20S-V11项目软硬件全开源

前言&#xff1a; 本文为4寸 480*480 RGB接口IPS全面触屏的86中控面板&#xff08;RT-ThreadLVGL&#xff09;软硬件开源干货内容&#xff0c;该项目是综合性非常强的RTOS系列项目&#xff01;项目主控芯片使用 Model 3c&#xff0c;整体实现了简化版本的86中控面板的功能需求…

apisix admin api 403 Forbidden(接口请求403)

故事背景 当你通过admin api 接口方式执行相关操作时&#xff0c;例如route、upstream设置&#xff0c;接口返回403 Forbidden&#xff0c; 例如 请求 curl -i "http://192.168.100.1:9180/apisix/admin/routes" -H X-API-KEY: edd1c9f034335f136f87ad84b625c8f1 -X…

微软 Power Platform 零基础 Power Apps 解决查找字段多选问题无需写代码

微软 Power Platform 零基础 Power Apps 解决查找字段多选问题无需写代码 在开发Power Apps产品的过程中&#xff0c;我们经常遇到查找字段多选的问题&#xff0c;只想用字段显示&#xff0c;又不想用子网格&#xff0c;我们今天来寻找一种不用开发的方式来实现这个功能。 效果…

「年终总结」生成人工智能的奇妙年份

自我介绍 做一个简单介绍&#xff0c;酒架年近48 &#xff0c;有20多年IT工作经历&#xff0c;目前在一家500强做企业架构&#xff0e;因为工作需要&#xff0c;另外也因为兴趣涉猎比较广&#xff0c;为了自己学习建立了三个博客&#xff0c;分别是【全球IT瞭望】&#xff0c;【…

登录不上linux keyboard-inter

登录不上linux keyboard-inter. 可以用其他工具测试下&#xff1a;cmd 删除历史记住密码&#xff1a;

go 源码解读 - sync.Mutex

sync.Mutex mutex简介mutex 方法源码标志位获取锁LocklockSlowUnlock怎么 调度 goroutineruntime 方法 mutex简介 mutex 是 一种实现互斥的同步原语。&#xff08;go-version 1.21&#xff09; &#xff08;还涉及到Go运行时的内部机制&#xff09;mutex 方法 Lock() 方法用于…

JavaScript练习题第(四)部分

大家好关于JavaScript基础知识点已经发布&#xff1a;需要的大家可以去我的主要查看 &#xff08;当然了有任何不会的&#xff0c;可以私信我&#xff01;&#xff01;&#xff01;&#xff01;&#xff09; 为了巩固大家学习知识点给大家准备几道练习题&#xff1a; 当然&…

GPT-4 API惨遭美国加州实验室团队毒手,清纯工具被进行攻击测试,经坑蒙拐骗黑化成坏蛋

美国加州实验室FAR AI的团队在对GPT-4 API进行安全测试时&#xff0c;采用了三大方向的“红队”攻击&#xff0c;结果让他们大吃一惊&#xff0c;GPT-4居然成功被越狱。通过对15个有害样本和100个良性样本的微调&#xff0c;他们成功地使GPT-4降低了警惕&#xff0c;使其生成有…

低代码平台在金融银行中的应用场景

随着数字化转型的推进&#xff0c;商业银行越来越重视技术在业务发展中的作用。在这个背景下&#xff0c;白码低代码平台作为一种新型的开发方式&#xff0c;正逐渐受到广大商业银行的关注和应用。白码低代码平台能够快速构建各类应用程序&#xff0c;提高开发效率&#xff0c;…

BAQ压缩MATLAB仿真

本专栏目录: ​​​​​​​全球SAR卫星大盘点与回波数据处理专栏目录-CSDN博客 我们按照上一期文章的BAQ原理编写MATLAB代码,进行baq压缩与解压缩的全流程验证,并分析BAQ压缩对信号指标造成的影响。 生成3个点目标回波数据,加入高斯噪声,对回波进行BAQ压缩和解BAQ压缩,…

Spring Boot + Mybatis + vue2 — 实现分页查询

后端 pom.xml文件导入依赖 <!--分页查询--> <dependency><groupId>com.github.pagehelper</groupId><artifactId>pagehelper-spring-boot-starter</artifactId><version>1.4.6</version> </dependency> 配置全局配置…

flink内存配置

flink内存配置 配置 TaskManager 内存 | Apache Flink