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

news2025/4/21 16:46:17

目录

  • 一、线程互斥
    • 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.coloradmin.cn/o/1346638.html

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

相关文章

Python编程-面向对象基础与入门到实践一书的内容拓展

Python编程-面向对象基础与入门到实践一书的内容拓展 通过编程&#xff0c;模拟现实生活中的事物编程&#xff0c;叫做面向对象编程&#xff0c;此过程也叫做实例化编程 简单类的创建 class Test():def __init__ (self,id):self.id iddef print_id(self):print(self.id)这里建…

数据结构入门到入土——List的介绍

目录 一&#xff0c;什么是List&#xff1f; 二&#xff0c;常见接口介绍 三&#xff0c;List的使用 一&#xff0c;什么是List&#xff1f; 在集合框架中&#xff0c;List是一个接口&#xff0c;继承自Collection。 Collection也是一个接口&#xff0c;该接口中规范了后序容…

【2023年终总结】谈谈一个新人眼里的阿里方法论

写在开头 2023年转眼就过去了&#xff0c;今年我从一名大学生转变某阿里系大厂的“搬砖打工人”&#xff0c;这一转变真的是给我“涉世未深的纯洁心灵”带来了大大的“震撼”。 角色的转变是需要时间进行“内部消化”的。无论是对于个人的价值认知或者是行为方式来说&#xf…

计算机网络【DNS】

DNS 基本概述 与 HTTP、FTP 和 SMTP 一样&#xff0c;DNS 协议也是应用层的协议&#xff0c;DNS 使用客户-服务器模式运行在通信的端系统之间&#xff0c;在通信的端系统之间通过下面的端到端运输协议来传送 DNS 报文。但是 DNS 不是一个直接和用户打交道的应用。DNS 是为因特…

Linux---进程控制

一、进程创建 fork函数 在Linux中fork函数是非常重要的函数&#xff0c;它从已存在进程中创建一个新进程&#xff0c;原进程为父进程 fork函数的功能&#xff1a; 分配新的内存和内核数据结构给子进程将父进程部分数据结构内容拷贝至子进程添加子进程到系统的进程列表中fork返…

Redis 快速搭建与使用

文章目录 1. Redis 特性1.1 多种数据类型支持1.2 功能完善1.3 高性能1.4 广泛的编程语言支持1.5 使用简单1.6 活跃性高/版本迭代快1.7 I/O 多路复用模型 2. Redis发展历程3. Redis 安装3.1 源码安装3.1.1 下载源码包3.1.2 解压安装包3.1.3 切换到 Redis 目录3.1.4 编译安装 3.2…

X210 Linux开发板挂载NFS文件系统

软件版本 VirtualBox v7.0、Ubuntu 20.04.3 LTS 网络搭建 采用“路由器”“有线网”来将Linux开发板和Ubuntu虚拟机连接在同一个局域网中。具体接线如下&#xff1a; Linux开发板通过网线直接连接到“路由器”的LAN接口上&#xff0c;然后笔记本电脑通过Wifi与路由器连接。…

模型量化之AWQ和GPTQ

什么是模型量化 模型量化&#xff08;Model Quantization&#xff09;是一种通过减少模型参数表示的位数来降低模型计算和存储开销的技术。一般来说&#xff0c;模型参数在深度学习模型中以浮点数&#xff08;例如32位浮点数&#xff09;的形式存储&#xff0c;而模型量化可以…

FL Studio怎么破解?FL Studio安装破解使用图文教程

fl studio是一款功能强大的编曲软件&#xff0c;怎么破解呢&#xff1f;今天小编就为大家带来了详细的安装破解教程&#xff0c;需要的朋友一起看看吧 fl studio20.8是一款功能强大的编曲软件&#xff0c;也就是众所熟知的水果软件。它可以编曲、剪辑、录音、混音&#xff0c;…

Python+Django+Mysql+SimpleUI搭建后端用户管理系统(非常详细,每一步都清晰,列举了里面所有使用的方法属性)

一、在Anaconda环境下创建虚拟环境 &#xff08;1&#xff09;打开Anaconda Prompt(install)&#xff0c;创建虚拟环境&#xff0c;如下图所示&#xff1a; 方法一&#xff1a;默认情况下虚拟环境创建在Anaconda安装目录下的envs文件夹中 conda create --name usermanage …

最优轨迹生成(一)—— 微分平坦

本系列文章是学习深蓝学院-移动机器人运动规划课程第五章最优轨迹生成 过程中所记录的笔记&#xff0c;本系列文章共包含四篇文章&#xff0c;依次介绍了微分平坦特性、无约束BVP轨迹优化、无约束BIVP轨迹优、 带约束轨迹优化等内容 本系列文章链接如下&#xff1a; 最优轨迹生…

kivy中用anchrolayout

说明 AnchorLayout 是 Kivy 框架中用于管理界面元素位置的一种布局方式。AnchorLayout 的特点是&#xff0c;它可以将其子元素锚定到布局的边界上&#xff0c;例如顶部、底部、左侧或右侧。这使得在需要元素相对于其容器边界保持固定位置时非常有用。 界面 # mylayout.kvAnch…

【Android Gradle 插件】ProductFlavor 配置 ( ProductFlavor 引入 | ProductFlavor 参考文档地址 )

Android Plugin DSL Reference 参考文档 : 文档主页 : Android Plugin 2.3.0 DSL Reference android 模块配置文档 : AppExtension - Android Plugin 2.3.0 DSL Reference ProductFlavor 文档 : ProductFlavor - Android Plugin 2.3.0 DSL Reference 一、ProductFlavor 配置…

使用Vue.js实现手机系统检测和页面响应

题目&#xff1a;使用Vue.js实现手机系统检测和页面响应 摘要&#xff1a;本文将介绍如何使用Vue.js来检测用户的手机操作系统&#xff0c;并根据操作系统类型进行相应的页面响应。我们将通过编写一个Vue组件&#xff0c;利用用户代理字符串来检测设备类型&#xff0c;并根据检…

四、RK3588-Mobilenet直接推理(C++版本)

1.前言 RKNN&#xff08;Rockchip Neural Network&#xff09;是一种用于嵌入式设备的深度学习推理框架&#xff0c;提供了一个端到端的解决方案&#xff0c;用于将训练好的深度学习模型转换为在嵌入式设备上运行的可执行文件。RKNN在Rockchip NPU&#xff08;神经网络处理器&a…

MySQL基础入门(二)

多表内容 一对多 这个内容是黑马的入门问题&#xff0c;可以带大家思考一下这个怎么设计 我们要知道一个岗位可以对应很多用户&#xff0c;而一个用户只能对应一个岗位&#xff0c;这就属于一对多的类型 那么我们需要怎么将他们进行关联呢&#xff1f; 现在我们可以通过一个…

【数据结构—堆排序、top k问题和时间复杂度】

提示&#xff1a;文章写完后&#xff0c;目录可以自动生成&#xff0c;如何生成可参考右边的帮助文档 文章目录 前言 一、堆排序&#xff08;升序&#xff09; 1.1向上调整算法&#xff1a; 1.2向下调整算法&#xff1a; 1.3、堆排序的实现&#xff1a; 二、top k问题 2.1top k…

x-cmd pkg | 比 du 更好用的查看磁盘空间工具:dust

目录 简介首次用户技术特点竞品和相关作品进一步探索 简介 Dust 是一个用于分析磁盘空间使用情况的命令行工具&#xff0c;旨在提供直观的磁盘分布信息&#xff0c;它的名字是由 “du” 和 Rust 编程语言组合而来。 Dust 的主要功能是提供实时的磁盘空间概览&#xff0c;并以…

文心一言 VS 讯飞星火 VS chatgpt (168)-- 算法导论13.2 1题

一、用go语言&#xff0c;写出 RIGHT-ROTATE 的伪代码。 文心一言&#xff1a; 在计算机科学中&#xff0c;右旋转&#xff08;RIGHT-ROTATE&#xff09;是一种常见的操作&#xff0c;通常用于数组或链表。以下是一个使用Go语言实现右旋转的伪代码&#xff1a; func rightRo…

程序员绩效考核表就是这么简单

大家好&#xff0c;今天给大伙分享一份程序员的绩效考核表&#xff0c;仅供参考&#xff01; 《程序员绩效考核表》