多线程(线程同步和互斥+线程安全+条件变量)

news2024/11/16 13:47:48

线程互斥

线程互斥:
任何时刻,保证只有一个执行流进入临界区访问临界资源,通常对临界资源起到保护作用

相关概念

  • 临界资源: 一次仅允许一个进程使用的共享资源
  • 临界区: 每个线程内部,访问临界资源的代码,就叫做临界区
  • 原子性: 不会被任何调度机制打断的操作,该操作只有两态(无中间态,即使被打断,也不会受影响),要么完成,要么未完成

互斥量mutex

概念:
多个线程对一个共享变量进行操控时,会引发数据不一致的问题。此时就引入了互斥量(也叫互斥锁)的概念,来保证共享数据操作的完整性。在被加锁的任一时刻,临界区的代码只能被一个线程访问。

为了更好的阐述这个概念,这里用一个抢票代码去演示

#include <iostream>
#include <vector>
#include <cstdio>
#include <cstring>
#include <string>
#include <unistd.h>
#include <cassert>
#include <pthread.h>
int ticket=1000;
void* getTicket(void* args)
{
    long id=(long) args;
    while(1)
    {
        if(ticket>0)
        {
            usleep(1000);
            --ticket;
            printf("thread %ld get a ticket,the number is %d\n",id,ticket);
        }
        else
        {
            break;
        }

    }
}
int main()
{
    //创建五个线程
    pthread_t t1[5];
    for(int i=0;i<5;++i)
    {
        pthread_create(&t1[i],nullptr,getTicket,(void*)i);
    }
    //主线程在阻塞等待
    for(int i=0;i<5;++i)
    {
        pthread_join(t1[i],nullptr);
    }
    return 0;
}

运行结果如下:

在这里插入图片描述

我们发现票到负数了还会继续执行

原因如下:

  • if 语句判断条件为真以后,代码可以并发的切换到其他线程
  • usleep 这个过程中,ticket还没有进行--的操作有很多线程会进入if条件
  • –-ticket 操作本身就不是一个原子操作ticket有三条汇编指令(如下):
movl  ticket(%rip), %eax     # 把ticket的值(内存)加载到eax寄存器中                                                                                                     
subl  $1, %eax               # 把eax寄存器中的值减1
movl  %eax, ticket(%rip)     # 把eax寄存器中的值赋给ticket变量

有可能在你执行到第二条汇编的时候,还没来得及拷贝给内存,就别切换走了,就会导致减到负数,因为别的线程在读取的时候内存中的ticket还是1

如何解决上述问题?

  • 代码必须要有互斥行为:当代码进入临界区执行时,不允许其他线程进入该临界区。
  • 如果多个线程同时要求执行临界区的代码,并且临界区没有线程在执行,那么只能允许一个线程进入该临界区。
  • 如果线程不在临界区中执行,那么该线程不能阻止其他线程进入临界区。

在这里插入图片描述

在临界区内,线程只能串行执行,在临界区外,线程可以并发执行

互斥量的接口

互斥量其实就是一把锁,是一个类型为pthread_mutex_t的变量,使用前需要进行初始化操作,使用完之后需要对锁资源进行释放

初始化互斥量:

全局锁或静态锁:pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER

局部锁:

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

参数:
restrict mutex:要初始化的锁
restrict attr:不关心,置空
返回值:
成功返回0,失败返回错误码
注意:
互斥量处于未锁状态,该函数会将互斥量锁定,同时返回成功
函数调用时,其他线程已经锁定互斥量,或者存在其他线程同时申请互斥量,但没有竞争到互斥量,那么pthread_ lock调用会陷入阻塞(执行流被挂起),等待互斥量解锁再去竞争锁

加锁:

int pthread_mutex_lock(pthread_mutex_t *mutex);

参数:
mutex:要加的锁
返回值:
成功返回0,失败返回错误码

解锁:

int pthread_mutex_unlock(pthread_mutex_t *mutex);

参数:

mutex:要解的锁

返回值:
成功返回0,失败返回错误码

销毁互斥量:

int pthread_mutex_destroy(pthread_mutex_t *mutex);

参数:
mutex:要销毁锁
返回值:
成功返回0,失败返回错误码

注意:

  • 不要销毁一个已经加锁的互斥量
  • 已经销毁的互斥量,要确保后面不会有线程再尝试加锁
  • 加锁的粒度要够小

用以上的方法再需要的地方进行加锁

#include <iostream>
#include <vector>
#include <cstdio>
#include <cstring>
#include <string>
#include <unistd.h>
#include <cassert>
#include <pthread.h>
using namespace std;
int ticket=1000;
class ThreadData
{
public:
    ThreadData(const string& threadname,pthread_mutex_t* mutex)
            :thread_name(threadname)
            ,mutex_p(mutex)
    {}
    string thread_name;
    pthread_mutex_t* mutex_p;
};
//创建并初始化
//全局的锁这样写可以不用初始化和销毁
// pthread_mutex_t mutex=PTHREAD_ MUTEX_ INITIALIZER;
void* getTicket(void* args)
{
    ThreadData *td = static_cast<ThreadData *>(args);
    // ThreadData* td=(ThreadData*) args;
    while(1)
    {
        //加锁
        pthread_mutex_lock(td->mutex_p);
        if(ticket>0)
        {
            usleep(1000);
            cout << td->thread_name << " tickets is " << ticket << endl;
            --ticket;
            //解锁
            pthread_mutex_unlock(td->mutex_p);
        }
        else
        {
            //解锁
            pthread_mutex_unlock(td->mutex_p);
            break;
        }
        //抢完票就完了吗?需要形成订单给用户
        //这里如果不休息,会一直是第4个线程在跑,原因是锁只规定互斥访问,没有规定必须让谁优先执行
        //锁就是真是的让多个执行流进行竞争的结果
        usleep(1000);
    }
}

int main()
{
    //创建五个线程
    pthread_t t1[5];
    pthread_mutex_t lock;
    pthread_mutex_init(&lock,nullptr);
    for(int i=0;i<5;++i)
    {
        char buffer[64];
        snprintf(buffer,sizeof(buffer),"%s""%d","thread ",i+1);
        //锁用同一把
        ThreadData* td=new ThreadData(buffer,&lock);
        pthread_create(&t1[i],nullptr,getTicket,td);
    }
    for(int i=0;i<5;++i)
    {
        pthread_join(t1[i],nullptr);
    }
    pthread_mutex_destroy(&lock);
    return 0;
}

运行结果如下:

在这里插入图片描述

这里运行会变慢,因为加锁以后是串行执行!

如何看待锁?

锁本身就是一个共享资源,全局变量是要被保护的,锁用来保护全局资源,锁本身也是全局资源,所以加锁的过程必须是安全的!加锁的过程是**原子的,锁如果申请成功,继续向后执行,**如果暂时没有申请成功,执行流会阻塞

如果锁申请成功,进入临界资源,正在访问临界资源,其他线程正在做什么?

阻塞等待

如果锁申请成功,进入临界资源,正在访问临界资源,我可以被切换吗?

可以,当持有线程的锁被切走,其他线程依旧无法申请锁成功,也无法向后执行,直到我释放这个锁

互斥量的原理

大多数体系结构都提供了swap或exchange指令,该指令的作用是把寄存器和内存单元的数据相交换,由于只有一条指令,保证了原子性

下面是lock和unlock的伪代码

lock:
	movb $0, %a1     # 把0值放进寄存器a1里
	xchgb %a1, mutex # 交换a1寄存器的内容和锁的值(无线程使用锁时,metux的值为1if (%a1 > 0)
		return 0; # 得到锁
	else
		挂起等待;
	goto lock;
unlock:
	movb $1 mutex  #把1赋给锁	
	唤醒等待的线程;
	return 0;

下图展示了如何实现:

在这里插入图片描述

解锁的伪代码步骤(只有有锁的线程才可以执行到这段代码):

  1. 把mutex的值改为1
  2. 唤醒等待锁的线程

封装锁

Mutex.hpp

#pragma once

#include <iostream>
#include <pthread.h>

class Mutex
{
public:
    Mutex(pthread_mutex_t *lock_p = nullptr): lock_p_(lock_p)
    {}
    void lock()
    {
        if(lock_p_) pthread_mutex_lock(lock_p_);
    }
    void unlock()
    {
        if(lock_p_) pthread_mutex_unlock(lock_p_);
    }
    ~Mutex()
    {}
private:
    pthread_mutex_t *lock_p_;
};

class LockGuard
{
public:
    LockGuard(pthread_mutex_t *mutex): mutex_(mutex)
    {
        mutex_.lock(); //在构造函数中进行加锁
    }
    ~LockGuard()
    {
        mutex_.unlock(); //在析构函数中进行解锁
    }
private:
    Mutex mutex_;
};

test.cpp

#include <iostream>
#include <vector>
#include <cstdio>
#include <cstring>
#include <string>
#include <unistd.h>
#include <pthread.h>
#include <memory>
#include <cassert>

#include "Mutex.hpp"

// 共享资源, 火车票
int tickets = 10000;
pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;
void *getTicket(void *args)
{
    long long username = (long long)args;
    while (true)
    {
        {
            //出了作用域会销毁
            LockGuard lockguard(&lock); // RAII风格的加锁!
            if (tickets > 0)
            {
                usleep(1254); 
                std::cout << username << " 正在进行抢票: " << tickets << std::endl;
                tickets--;
            }
            else
            {
                break;
            }
        }
        usleep(1000); 
    }

    return nullptr;
}
int main()
{

#define NUM 4
    
    pthread_t t1[5];
    for(int i=0;i<5;++i)
    {
        pthread_create(&t1[i],nullptr,getTicket,(void*)i);
    }
    //主线程保持运行
    for(int i=0;i<5;++i)
    {
         pthread_join(t1[i],nullptr);
    }
    return 0;
}

线程安全和可重入

概念

线程安全: 多个线程并发同一段代码时,不会出现不同的结果。常见对全局变量或者静态变量进行操作,并且没有锁保护的情况下,会出现该问题。
重入: 同一个函数被不同的执行流调用,当前一个流程还没有执行完,就有其他的执行流再次进入,我们称之为重入。一个函数在重入的情况下,运行结果不会出现任何不同或者任何问题,则该函数被称为可重入函数,否则,是不可重入函数。

常见的线程安全的情况

  • 每个线程对全局变量或者静态变量只有读取的权限,而没有写入的权限,一般来说这些线程是安全的
  • 类或者接口对于线程来说都是原子操作
  • 多个线程之间的切换不会导致该接口的执行结果存在二义性

常见的线程不安全的情况

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

常见可重入的情况

  • 不使用全局变量或静态变量
  • 不使用用malloc或者new开辟出的空间
  • 不调用不可重入函数
  • 不返回静态或全局数据,所有数据都有函数的调用者提供
  • 使用本地数据,或者通过制作全局数据的本地拷贝来保护全局数据

常见不可重入的情况

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

区别与联系

区别:

  • 函数是可重入的,那就是线程安全的
  • 函数是不可重入的,那就不能由多个线程使用,有可能引发线程安全问题
  • 如果一个函数中有全局变量,那么这个函数既不是线程安全也不是可重入的

联系:

  • 可重入函数是线程安全函数的一种
  • 线程安全不一定是可重入的(不一定发生线程安全问题),而可重入函数则一定是线程安全的。
  • 如果将对临界资源的访问加上锁,则这个函数是线程安全的,但如果这个重入函数若锁还未释放则会产生死锁,因此是不可重入的。

死锁

概念

死锁是指两个或两个以上的进程在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞的现象,若无外力作用,它们都将无法推进下去。此时称系统处于死锁状态或系统产生了死锁,这些永远在互相等待的进程称为死锁进程。

死锁产生的四个必要条件:

  • 互斥条件:一个资源每次只能被一个执行流使用
  • 请求与保持条件:一个执行流因请求资源而阻塞时,对已获得的资源保持不放
  • 不剥夺条件:一个执行流已获得的资源,在未使用完之前,不能强行剥夺
  • 循环等待条件:若干执行流之间形成一种头尾相接的循环等待资源的关系

所谓的必要条件是都要满足才能形成死锁,只要有一个不满足就不是死锁

避免死锁

  • 破坏死锁的四个条件(上面分别对应的是:1.不使用锁 2.让一个执行流放开资源 3. 让一个个执行流剥夺一个执行流的资源 4. 调整申请资源的顺序)
  • 假设顺序要一致
  • 避免锁未释放的场景
  • 资源一次性分配

避免死锁算法:

  • 银行家算法:为了防止银行家资金无法周转而倒闭,对每一笔贷款,必须考察其是否能限期归还。在操作系统中研究资源分配策略时也有类似问题,系统中有限的资源要供多个进程使用,必须保证得到的资源的进程能在有限的时间内归还资源,以供其他进程使用资源。
  • 死锁检测法

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

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

相关文章

信息抽取与命名实体识别:从原理到实现

❤️觉得内容不错的话&#xff0c;欢迎点赞收藏加关注&#x1f60a;&#x1f60a;&#x1f60a;&#xff0c;后续会继续输入更多优质内容❤️ &#x1f449;有问题欢迎大家加关注私戳或者评论&#xff08;包括但不限于NLP算法相关&#xff0c;linux学习相关&#xff0c;读研读博…

STM32-江科大

新建工程 引入启动文件 Start中是启动文件&#xff0c;是STM32中最基本的文件&#xff0c;不需要修改&#xff0c;添加即可。 启动文件包含很多类型&#xff0c;要根据芯片型号来进行选择&#xff1a; 如果是选择超值系列&#xff0c;那就使用带 VL 的启动文件&#xff0c;…

多元统计分析-主成分分析的原理与实现

目录 一、什么是主成分分析&#xff1f; 二、主成分分析的原理 三、主成分分析的应用 四、使用sklearn实现主成分分析 五、总结 一、什么是主成分分析&#xff1f; 主成分分析&#xff08;Principal Component Analysis&#xff0c;PCA&#xff09;是一种常用的多元统计分…

Docker部署FAST OS DOCKER容器管理工具

Docker部署FAST OS DOCKER容器管理工具 一、FAST OS DOCKER介绍1. FAST OS DOCKER简介2. FAST OS DOCKER特点 二、本次实践介绍1. 本次实践简介2. 本次实践环境 三、本地环境检查1.检查Docker服务状态2. 检查Docker版本 四、下载FAST OS DOCKER镜像五、部署FAST OS DOCKER1. 创…

理解控制变量、内生变量、外生变量、工具变量

文章目录 前言一、控制变量二、内生变量、外生变量三、工具变量&#xff08;IV&#xff09; 前言 1.解释变量&#xff08;或自变量&#xff09;&#xff1a;解释变量是指作为研究对象&#xff0c;用于解释某个现象或行为模式的变量。其中有些解释变量是直接影响被解释变量的&a…

自学黑客(网络安全),一般人我劝你还是算了吧

一、自学网络安全学习的误区和陷阱 1.不要试图先成为一名程序员&#xff08;以编程为基础的学习&#xff09;再开始学习 我在之前的回答中&#xff0c;我都一再强调不要以编程为基础再开始学习网络安全&#xff0c;一般来说&#xff0c;学习编程不但学习周期长&#xff0c;而…

重塑未来:AI对教育行业的深远影响与挑战

自从AI人工智能的发展进入“iPhone时刻”以来&#xff0c;我们已身处一个日新月异的时代。在众多领域&#xff0c;AI已经大放异彩&#xff0c;而教育作为培养下一代的关键领域&#xff0c;自然也受到了这场科技革命的影响。 AI对教育行业重大影响 最近可汗学院&#xff08;Kh…

图论网络模型及求最小路径和造价实战

学习知识要实时简单回顾&#xff0c;我把学习的图论简单梳理一下&#xff0c;方便入门与复习。 图论网络 图论网络简介 图论起源于 18 世纪。第一篇图论论文是瑞士数学家欧拉于 1736 年发表的“哥尼斯堡的七座桥”。1847 年&#xff0c;克希霍夫为了给出电网络方程而引进了“…

《Netty》从零开始学netty源码(五十五)之PooledByteBufAllocator

PooledByteBufAllocator 通过前面的学习我们大体了解了PooledByteBufAllocator管辖下的数据结构&#xff0c;整体情况如下&#xff1a; PooledByteBufAllocator主要管理了三类内存&#xff0c;堆内存heapArenas、直接内存directArenas、线程缓存PoolThreadCache&#xff0c;前…

Java笔记_18(IO流)

Java笔记_18 一、IO流1.1、IO流的概述1.2、IO流的体系1.3、字节输出流基本用法1.4、字节输入流基本用法1.5、文件拷贝1.6、IO流中不同JDK版本捕获异常的方式 二、字符集2.1、GBK、ASCII字符集2.2、Unicode字符集2.3、为什么会有乱码2.4、Java中编码和解码的代码实现2.5、字符输…

直方图均衡化与规定化原理解释以及matlab实现

直方图均衡化(HE) Histogram Equalization (HE) 设灰度水平在 r k , k ∈ [ 0 &#xff0c; L − 1 ] r_k,k\in[0&#xff0c;L-1] rk​,k∈[0&#xff0c;L−1] 内 一幅图像 f f f 的非归一化直方图定义为 h ( r k ) n k h(r_k)n_k h(rk​)nk​ s T ( r ) sT(r) sT(r)为…

【统计模型】心脏病患病影响因素探究

目录 心脏病患病影响因素探究 一、研究目的 二、数据来源和相关说明 三、描述性统计分析 四、数据建模 4.1 全模型 &#xff08;1&#xff09;模型构建 &#xff08;2&#xff09;模型预测 4.2 基于AIC准则的选模型A 4.3 基于BIC准则的选模型B 4.4 模型评估 五、结论…

Vector - CAPL - CANoe硬件配置函数 - 02

Hardware Configuration 硬件配置中包含CAN或者CANFD的参数配置&#xff0c;其中包含波特率、时间片1、时间片2、时间量子中的同步跳跃宽度、采样点数等信息&#xff1b;随着研发系统中各类型的平台化&#xff0c;测试想要跟上研发的进度&#xff0c;也必须进行平台化&#xff…

linux【网络编程】之网络套接字预备

linux【网络编程】之网络套接字 一、必备知识1.1 端口号1.2 端口号方面疑问及解决方案 二、TCP/UDP协议三、网络字节流四、socket编程4.1 认识接口4.2 浅析sockaddr结构 一、必备知识 在【网络基础】中我们提到了IP地址&#xff0c;接下来了解一下网络通信中其他方面的知识 1…

浏览器的渲染

浏览器的渲染 浏览器的渲染过程分为两大阶段&#xff0c;八大步骤&#xff0c;由两个线程完成&#xff0c; 下面是总的过程 第一个 渲染主线程 它包括5个步骤&#xff0c; 1、html解析 parse 解析我们的HTML&#xff0c;生成DOM树结构 2、样式计算 computed style 比如我们…

系统运维(Git篇)

Git基础 Git Git是一种分布式版本控制系统&#xff0c;可以帮助我们管理代码的版本和变更。通过学习Git&#xff0c;我们可以更好地理解版本控制的原理和应用&#xff0c;同时也可以掌握Git的使用和管理技巧。 Docker Docker是一种容器化平台&#xff0c;可以将应用程序及其依赖…

华为OD机试真题2023(JAVA)

目录 华为OD机试是什么&#xff1f;华为OD面试流程&#xff1f;华为OD机试通过率高吗&#xff1f;华为OD薪资待遇&#xff1f;华为OD晋升空间&#xff1f; 大家好&#xff0c;我是哪吒。 本专栏包含了最新最全的华为OD机试真题&#xff0c;有详细的分析和Java代码解答。已帮助…

web前端的同源策略是什么?

一、同源策略 1995年&#xff0c;同源政策由 Netscape 公司(网景公司)引入浏览器。目前&#xff0c;所有浏览器都实行这个政策。同源政策的目的&#xff0c;是为了保证用户信息的安全&#xff0c;防止恶意的网站窃取数据。随着互联网的发展&#xff0c;“同源政策”越来越严格…

深入理解java虚拟机精华总结:运行时栈帧结构、方法调用、字节码解释执行引擎

深入理解java虚拟机精华总结&#xff1a;运行时栈帧结构、方法调用、字节码解释执行引擎 运行时栈帧结构局部变量表操作数栈动态连接方法返回地址 方法调用解析分派静态分派动态分派 基于栈的字节码解释执行引擎 运行时栈帧结构 Java虚拟机以方法作为最基本的执行单元&#xf…

栈在表达式中的应用(中/后前缀的转换)机算,手算模拟。

一.中缀表达式转后缀表达式 初始化一个栈&#xff0c;用于保存 暂时还不确定的运算顺序的“运算符” 。 从 左往右 依次扫描&#xff0c;会遇到三种情况&#xff1a; 1.遇到 操作数&#xff0c;直接加入后缀表达。 2.遇到 界限符&#xff1a;     ①遇到 “(” 入栈。  …