线程同步方式之一互斥锁

news2024/11/24 20:48:17

线程同步的4种方式:互斥锁、条件变量、读写锁、信号量

了解概念-临界资源、互斥、临界区、原子性

回想一下在信号量那部分提起过的几个概念,将多个执行流串行安全访问的共享资源称为临界资源,多个执行流中访问临界资源的代码所在的地址空间称为临界区,临界区往往是线程代码的一两句,想让多个线程串行访问共享资源,保护方法之一是互斥

要么不做,要么就是做完,只有两态,称为原子性。只用一条汇编语句就能完成对资源进行操作,就是原子性操作,否则就不是原子性操作(++/–要用三条汇编)

代码必须要有互斥行为指的是1、当线程A进入临界区执行时,不允许其他线程进入该临界区;2、如果多个线程同时要求执行临界区的代码,并且临界区没有线程在执行,那么只能允许一个线程进入该临界区;3、如果线程不在临界区中执行,那么该线程不能阻止其他线程进入临界区。
要做到这三点,本质上就是需要一把锁。Linux上提供的这把锁叫互斥量,也叫互斥锁。

thread mutex互斥锁

线程栈是由线程库帮用户维护的

场景:多线程购票–负数票

场景:购票系统。1000张票==>设为全局变量。让每个线程充当一个抢票用户,然后多线程同时对一个全局变量进行操作,实验现象:当票数<0时,一些线程仍显示抢负数票成功。

要看到上述现象,要求代码中让多个线程交叉执行(本质是让调度器尽可能频繁发生线程调度),才会发生对全局数据判断出错。

线程发生切换的条件

线程一般在什么时候发生切换呢?

1、时间片到了;2、更高优先级的线程来了;3、线程等待的时候。

线程怎么判断自己要被切换了呢?

该线程从内核态返回用户态的时候,线程要对调度状态进行检测,如果可以,就直接发生线程切换。

#include "/home/yyq/linux-class/2023_03_23_ThreadMutex/Thread.hpp"

#include <memory>
#include <unistd.h>

int tickets = 1000;

void* getTicket(void* args)
{
    std::string username = static_cast<const char*>(args);
    while(1)
    {
        if(tickets > 0)
        {
            //模拟真实抢票要花费的时候
            usleep(12345);//1秒 = 1000毫秒= 1000 000微秒
            std::cout << username << "正在抢票 " << tickets << std::endl;
            
            tickets--;
        }
        else
        {
            std::cout << "=====没票了=====" << std::endl;
            break;
        }
        usleep(1000);
    }
    return nullptr;
}

int main()
{
    std::unique_ptr<Thread> thread1(new Thread(1, getTicket, (void*)"user1..."));
    std::unique_ptr<Thread> thread2(new Thread(2, getTicket, (void*)"user2..."));
    std::unique_ptr<Thread> thread3(new Thread(3, getTicket, (void*)"user3..."));
    std::unique_ptr<Thread> thread4(new Thread(4, getTicket, (void*)"user4..."));

    thread1->join();
    thread2->join();
    thread3->join();
    thread4->join();
    return 0;
}

每个线程都会先执行usleep();,属于3线程等待的时候这个切换条件。某个线程休眠的时候,其他线程正在抢票,当该线程被唤醒时,就容易发生负数抢票的情况。

++/–在内核中做了什么

  • 判断的本质逻辑是,1、读取内存数据到cpu的寄存器中;2、加法器判断(tickets+(-1)>0);
  • tickets--做的是:1、读取数据;2、更改数据;3、写回数据;

– 操作并不是原子操作,而是对应三条汇编指令:

  • load :将共享变量ticket从内存加载到寄存器中
  • update : 更新寄存器里面的值,执行-1操作
  • store :将新值,从寄存器写回共享变量ticket的内存地址

负数票原因分析

分析:当线程1进去的时候,把tickets读到寄存器里,值为1,然后线程2进来了,线程1进入休眠就被切换,带着自己的上下文就走了;线程2也读到1,线程3也读到1,线程4也读到1…每个线程都认为还有1张票。线程1醒过来了,要再去读一下tickets,还是1,打印然后–(读取+更改+写回),tickets变成0;线程2醒过来,再去读一下tickets,是0,打印然后–(读取+更改+写回),tickets变成-1;线程3醒过来,再去读一下tickets,是-1,打印然后–(读取+更改+写回),tickets变成-2;线程4醒过来,再去读一下tickets,是-2,打印然后–(读取+更改+写回),tickets变成-3,所以出现了票数为负数的情况!

问题:当tickets==1时,这4个线程能不能同时进入到getTicket函数?可以。能不能同时进行if(tickets > 0)条件判断?不可以。

对一个全局变量进行多线程更改是安全的吗?如果操作是原子的,则是安全的。++/–不是原子操作。对变量进行++或者–操作,在C/C++的代码虽然只有一条,但汇编以后至少是3条语句(1、读取内存数据到cpu的寄存器中;2、在寄存器中让cpu进行对应的算逻运算;3、将寄存器新的数据写回内存对应位置),这3步任何一步被打断,都要从这一步重新执行。

我们定义的全局变量在没有被保护的情况下,多线程交替执行造成的数据安全问题==>数据不一致。

如何解决–加锁

mutex互斥量 – 锁

pthread_mutex_lockpthread_mutex_unlock 之间保护起来的得是临界区,用锁保护的区域没写对也不行。

#include <pthread.h>

int pthread_mutex_init(pthread_mutex_t *restrict mutex, const pthread_mutexattr_t *restrict attr);
int pthread_mutex_destroy(pthread_mutex_t *mutex);
//局部锁要用init初始化和destory销毁
-----------------
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;//这样定义的是全局锁,PTHREAD_MUTEX_INITIALIZER表示初始化,不用再调用初始化pthread_mutex_init和销毁pthread_mutex_destroy函数
-----------------
int pthread_mutex_lock(pthread_mutex_t *mutex);//阻塞式申请加锁
int pthread_mutex_trylock(pthread_mutex_t *mutex);//非阻塞式申请加锁
int pthread_mutex_unlock(pthread_mutex_t *mutex);
//在lock和unlock之间的代码就是临界区
mutex输出型参数

互斥锁的使用方法

初始化互斥锁

//方法1,静态分配
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
//方法2,动态分配
int pthread_mutex_init(pthread_mutex_t *restrict mutex, const pthread_mutexattr_t *restrictattr);

trylock

返回0表示成功加锁;否则返回错误码,表示此时有线程已经加锁了。即非阻塞式加锁,可以避免死锁。

销毁互斥锁

销毁互斥量需要注意:

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

如何看待锁?

多个线程是用锁来进行互斥的,锁用来保护这部分全局资源。锁本身就是共享资源,也是临界资源。所以要求pthread_mutex_lock 加锁的过程必须是安全的。

加锁的过程是原子的!

  • 要么申请成功,执行流进入临界区,访问临界资源;
  • 要么申请不成功,此时执行流会阻塞(挂起状态),直到锁被释放,执行流再被唤醒。

执行流持有锁后,才能进入临界区。多线程中,当线程1申请锁成功进入临界区,其余线程处于阻塞状态,且线程1可以被切换。当持有锁的线程被切换时,其余线程依旧无法成功申请锁,直到该线程主动解锁。

对于其他线程而言,有意义的锁的状态,就两种:申请锁前、释放锁后(因为其他线程此时可以成功申请锁),持有锁时这个状态无意义的(因为其他线程此时没法申请锁,只能阻塞)==> 所以站在其他线程的角度看待当前线程持有锁的过程就是原子的。

未来我们在使用锁的时候,一定要尽量保证临界区的粒度非常小。既安全保护了共享资源,又不降低多线程运行速度。(线程加锁,要么全部线程都加,要么全部都不加)

锁的安全由谁来保护?

加锁和解锁的底层实现

加锁和解锁的过程是多个线程串行执行的。锁只规定了互斥访问,没有规定让哪个执行流优先执行,故谁能先加锁是多条执行流竞争的结果。

加锁,会让所有执行流串行访问临界区,从而达到互斥的目的。

为了实现互斥锁操作,大多数体系结构都提供了swapexchange指令,该指令的作用是把寄存器和内存单元的数据相交换,由于只有一条指令,保证了原子性。即使是多处理器平台,访问内存的总线周期也有先后,一个处理器上的交换指令执行时另一个处理器的交换指令只能等待总线周期。

lock:
	movb $0, %al
    xchgb %al, mutex
    if(al寄存器内容 > 0){
        return 0;
    }else 
        等待挂起;
    goto lock;

unlock:
	movb $1, mutex
    唤醒等待Mutex的线程;
	return 0;

共识:CPU内寄存器只有一套,是被所有执行流共享的,但是CPU寄存器上的内容是每个执行流私有的!

这里的0表示锁已被申请,1表示锁未被申请;%al表示寄存器里的内容;进程初始化锁,就相当于再自己的内存单元放了1。

lock–加锁的过程:

movb $0, %al :表示把0放到寄存器里 ==> 如果此时被切换,这条语句就是将0放在当前线程的上下文里。如果此时线程被切换了,也不用担心,该进程会带走自己的上下文;

xchgb %al, mutex :表示交换寄存器和内存单元的数据进行交换,而mutex是共享变量(当前值为1),一条汇编指令就完成了,将共享的mutex数据(值为1)交换到寄存器(当前值为0)里,那么寄存器里的值就是1,进程的内存单元中共享数据为0,意思就是当前线程把锁拿走了。如果此时该线程被切换了,1就会在当前线程的上下文中,此时别的线程在进程的共享单元也拿不到1,就会等待挂起,而持有1的线程执行return 0;可以继续执行。

unlock–解锁的过程:

movb $1, mutex:表示把1放到内存单元中的共享数据mutex里,相当于归还锁。

加锁后的多线程购票代码

#include <iostream>
#include <string>
#include <cstring>
#include <cassert>
#include <pthread.h>
#include <vector>
#include <memory>
#include <unistd.h>
//封装一个大号结构体,把信息给线程 -- 局部锁
class ThreadData
{
public:
    ThreadData(const std::string threadname, pthread_mutex_t* pmutex)
    : _threadName(threadname), _pmutex(pmutex)
    {}
    ~ThreadData(){}
public:
    std::string _threadName;
    pthread_mutex_t* _pmutex;
};

int tickets = 1000;

void *getTicket(void *args)
{
    ThreadData* td = static_cast<ThreadData *>(args);
    while (1)
    {
        pthread_mutex_lock(td->_pmutex);
        i (tickets > 0)
        {
            // 模拟真实抢票要花费的时候
            usleep(12345); // 1秒 = 1000毫秒= 1000 000微秒
            std::cout << td->_threadName << "正在抢票 " << tickets << std::endl;

            tickets--;
            pthread_mutex_unlock(td->_pmutex);
        }
        else
        {
            pthread_mutex_unlock(td->_pmutex);
            std::cout << "======没票了======" << std::endl;
            break;
        }
        usleep(1000);
    }
    return nullptr;
}

int main()
{
    // 创建,初始化线程
    pthread_mutex_t lock;
    pthread_mutex_init(&lock, nullptr);
#define NUM 4
    std::vector<pthread_t> tids(NUM);
    // 用系统接口写多线程抢票
    for(int i = 0; i < NUM; ++i)
    {
        char buffer[1024];
        snprintf(buffer, sizeof(buffer), "thread %d", i+1);
        ThreadData* td = new ThreadData(buffer, &lock);
        pthread_create(&tids[i], nullptr, getTicket, (void*)td);
    }

    for(const auto& tid : tids)
    {
        pthread_join(tid, nullptr);
    }

    pthread_mutex_destroy(&lock);
    return 0;
}

互斥锁的缺点:死锁

死锁是指在一组进程中的各个进程均占有不会释放的资源(1把锁及以上),但因互相申请被其他进程所站用不会释放的资源而处于的一种永久等待状态。

在有多把锁的场景下,持有自己的锁不释放,还要申请其他的锁,其他线程也是如此,这样很容易造成死锁。只有1把锁,也会造成死锁,比如线程A申请到了锁,A又去申请锁,自己就会被阻塞挂起,这个时候没人去释放这把锁,就变成死锁了。

多线程的特性是大部分资源包括全局资源是共享的,所以在进行多线程访问的时候会出现数据不一致的问题,为了保护临界资源的安全,我们就要用锁,由此会带来死锁的问题。

除了死锁,还存在重复锁定和解锁,每次都会检查共享数据结构,浪费时间和资源;繁忙查询的效率非常低等问题。

死锁的四个必要条件

4个条件均需满足

  1. 互斥条件:一个资源每次只能被一个执行流使用;(这是锁的基本特性)

在这里插入图片描述

  1. 请求与保持条件:一个执行流因请求资源而阻塞时,对已获得的资源保持不放;(线程A自己有锁,又去申请锁,进而无法释放)

在这里插入图片描述

  1. 不剥夺条件:一个执行流已获得的资源,在末使用完之前,不能强行剥夺;

在这里插入图片描述

  1. 循环等待条件:若干执行流之间形成一种头尾相接的循环等待资源的关系。(基于前3个条件造成的,如线程A有自己的锁,又去要线程B的锁;线程B有自己的锁,又去要线程C的锁;线程C有自己的锁,又去要线程A的锁;)

在这里插入图片描述

避免死锁的方法

死锁检测算法、银行家算法等都是依靠下面的方法

  1. 破坏死锁的四个必要条件。互斥条件和不可剥夺条件由共享资源本身的使用特性所决定的,因此不好破坏,相反还应加以保证;请求与保持条件可以用try_lock和及时释放锁破坏;不剥夺条件可以用优先级或状态破坏;循环等待条件可以用一致加锁顺序破坏。
  2. 加锁顺序一致;
  3. 避免锁未释放的场景;
  4. 资源一次性分配。

线程A申请的锁可以被线程B释放,但在实际应用中,最好是谁申请谁释放。

银行家算法:安全状态是非死锁状态,而不安全状态并不一定是死锁状态。即系统处于安全状态一定可以避免死锁,而系统处于不安全状态则仅仅可能进入死锁状态。银行家算法的实质就是要设法保证系统动态分配资源后不进入不安全状态,以避免可能产生的死锁。

RAII风格 封装锁

#pragma once

#include <iostream>
#include <thread>

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

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

在调用的时候直接创建LockGuard对象即可

LockGuard lockguard(&lock); // RAII风格 该对象的生命周期就只在循环体内,会自动析构解锁

可重入与线程安全

常见的线程不安全的情况

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

常见的可重入情况

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

可重入与线程安全联系

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

可重入与线程安全区别

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

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

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

相关文章

第4章 数据结构之“队列”

队列简介(queue) 1.一个先进先出的数据结构 2.javascript中没有这个数据结构&#xff0c;但是可以使用array实现队列的所有功能。 3.队列常用操作&#xff1a;push&#xff0c;shift&#xff0c;获取队列头部的元素&#xff1a;queue[0] const queue []// 入队&#xff1a; …

网络基础之网络传输基本流程

网络基础 此小节介绍网络基础概念 首先要明确的是 网络是层状结构&#xff01;分层->OP->解耦 网络发展&#xff1a;最早的时候&#xff0c;每台计算机之间是相互独立的。后续发展到网络互联&#xff0c;就是将多台计算机连接在一起&#xff0c;完成数据共享。 协议&…

Jmeter配置元件之csv数据文件配置

一、csv简介 csv是非常通用的一种文件格式&#xff0c;适用于批量导入数据到接口参数中&#xff0c;或者保存测试结果都可以使用csv数据文件(jmeter不仅仅支持这一种读取文件的方式)&#xff0c;csv数据一行即为数据表的一行&#xff0c;多个字段用逗号隔开。 登录接口 …

A+CLUB管理人支持计划第四期 | 香农投资

免责声明 本文内容仅对合格投资者开放&#xff01; 私募基金的合格投资者是指具备相应风险识别能力和风险承担能力&#xff0c;投资于单只私募基金的金额不低于100 万元且符合下列相关标准的单位和个人&#xff1a; &#xff08;一&#xff09;净资产不低于1000 万元的单位&…

Vue3技术7之toRaw与markRaw、customRef、provide与inject、响应式数据的判断、组合式API的优势分析

Vue3技术7 toRaw与markRawtoRawApp.vueDemo.vue markRawDemo.vue 总结 customRefApp.vue总结 provide与inject目录结构App.vueChild.vueSon.vue总结 响应式数据的判断App.vue总结 组合式API的优势配置式的API存在的问题组合式API的优势 toRaw与markRaw toRaw App.vue <te…

HDR tone mapping介绍

文章目录 HDR and tone mapping1.什么是HDR&#xff1f;2.为什么需要HDR&#xff1f;3.hdr文件格式4.tone mapping4.1 aces tone mapping4.2 Fast Bilateral Filtering for the Display of High-Dynamic-Range Images 5 参考 HDR and tone mapping 1.什么是HDR&#xff1f; 就…

nginx + springboot 实现限流

1.spring项目打成jar包后&#xff0c;运行起来 &#xff1a;例如我启动项目 ip 端口号&#xff1a;172.168.0.217:8090 2.修改nginx配置&#xff0c;增加如下配置 nginx 中有两个主要的指令可以用来配置限流&#xff1a;limit_req_zone 和 limit_req upstream myserver{serve…

面试篇:Redis

一、缓存穿透 1、缓存穿透 查询一个不存在的数据&#xff0c;mysql查询不到数据也不会直接写入缓存&#xff0c;就会导致每次请求都查数据库。即&#xff1a;大量请求根本不存在的key 2、查询流程 3、出现原因 业务层误将缓存和库中的数据删除了&#xff0c;也可能是有人恶…

3台服务器+StarVCenter,搭建“超融合云平台”-完美体验-跑100台虚拟机

3台服务器StarVCenter&#xff0c;搭建“超融合云平台”-完美体验-跑100台虚拟机 我们通常讲的“超融合&#xff08;HCI&#xff09;”是一种云平台基础架构方案&#xff0c;它无需专用的存储设备&#xff0c; 每台服务器既承担计算又存储数据&#xff0c; 只需增加服务器&…

SpringMVC-RESTful架构风格

目录 RESTful架构风格 1、RESTful概述 2、RESTful的六大原则 3、RESTful的实现 4、HiddenHttpMethodFilter RESTful风格的CRUD 1、环境搭建 2、功能需求 3、功能&#xff1a;访问首页 4、功能&#xff1a;查询所有数据 5、功能&#xff1a;删除一条数据 6、功能&…

Web服务

安装 WEB 服务&#xff1b; ~ 服务以用户 webuser 系统用户运行&#xff1b; ~ 限制 web 服务只能使用系统 500M 物理内存&#xff1b; ~ 全站点启用 TLS 访问&#xff0c;使用本机上的“CSK Global Root CA”颁 发机构颁发&#xff0c;网站证书信息如下&#xff1a; C…

财报解读:涅槃重生之后,新东方还想再造一个“文旅甄选”?

新东方逐渐走出了“微笑曲线”。 图源&#xff1a;新东方2023财年Q3财报 2023年4月19日&#xff0c;新东方披露了2023财年Q3财报&#xff08;截至2023年2月28日止&#xff09;&#xff0c;营收7.5亿美元&#xff0c;同比增长22.8%&#xff1b;归母净利润为8165万美元&#xff…

2023年4月份上新的视频领域分割模型设计系列论文(附下载链接)

来源&#xff1a;投稿 作者&#xff1a;王老师 编辑&#xff1a;学姐 论文1 论文标题&#xff1a; Boosting Video Object Segmentation via Space-time Correspondence Learning 论文链接&#xff1a; https://arxiv.org/pdf/2304.06211v1.pdf代码链接&#xff1a;暂未开源 …

QGIS数据可视化学习笔记02——CSV数据和表连接

在其他的GIS软件中&#xff0c;表的连接操作是十分常用的操作&#xff0c;在QGIS中也是一样的&#xff0c;接下来我们介绍QGIS中属性表之间的连接以及如何添加CSV数据到属性表中。 1、表的连接 &emsp如关系型数据库一样&#xff0c;两表连接的前提是&#xff0c;两个表中都…

使用大模型进行代码阅读——关于LLaMA模型代码的一些思考

使用大模型进行代码阅读 关于LLaMA模型代码的一些思考 关于这个模型的代码其实挺简单的&#xff0c;如果了解transformer decoder的代码&#xff0c;这个代码理解起来相对会比较容易一些&#xff0c;但是在这个代码里面有几个问题&#xff0c;是自己的一些思考或者是疑问点吧…

【NLP教程】用python调用百度AI开放平台进行情感倾向分析

一、背景 Hi&#xff0c;大家&#xff01;我是 马哥python说 &#xff0c;一名10年程序猿。 今天我来演示一下&#xff1a;通过百度AI开放平台&#xff0c;利用python调用百度接口进行中文情感倾向分析&#xff0c;并得出情感极性分为积极、消极还是中性以及置信度结果。 二…

HTB-Cache

HTB-Cache 信息收集80端口 立足www-data -> ashash -> luffyluffy -> root 信息收集 80端口 主页是一些hacker的介绍。 作者介绍能收集可能的用户名ASH以及有一个其他项目可能有用&#xff0c;如下&#xff1a; “Check out his other projects like Cache:HMS(Hosp…

【Python小技巧】使用Gradio 构建基于ChatGPT的AI绘图 Web 应用(附源码)

文章目录 前言一、Gradio是什么&#xff1f;二、使用Gradio构建基于ChatGPT的 Web 应用1. 安装gradio库2. 安装openai库&#xff08;ChatGPT的python库&#xff09;3. Web 应用示例&#xff08;源代码&#xff09; 总结 前言 都说ChatGPT也可以生成图片&#xff0c;好奇的我也…

母婴市场竞争激烈,如何通过软文营销脱颖而出

如今&#xff0c;随着宝宝数量增加以及人们对孩子的重视程度的增加&#xff0c;母婴市场愈发火爆。然而&#xff0c;母婴行业的竞争也越来越激烈&#xff0c;企业需要不断开拓新市场才能生存。在这样的情况下&#xff0c;软文营销成为了母婴企业拓展市场的一种有效方式。 首先&…

Docker Compose的安装教程、使用教程示例

Docker Compose的安装教程、使用教程示例 安装教程下载修改权限创建软链测试是否安装成功 使用教程1: 定义python应用2.创建容器的Dockerfile文件3.定义docker-compose脚本web容器&#xff1a;redis容器&#xff1a; 4.使用Compose构建并运行您的应用程序5.访问浏览器 安装教程…