【Linux-多线程】线程互斥(锁和它的接口等)

一、线程互斥

我们把多个线程能够看到的资源叫做共享资源,我们对共享资源进行保护,就是互斥

1.多线程访问问题

【示例】见一见多线程访问问题,下面是一个抢票的代码,共计票数10000张,4个线程去抢

之前我们展示过封装代码,这里我们直接使用

#include <iostream>
#include <vector>
#include <cstdio>
#include <unistd.h>
#include "mythread.hpp"

using namespace TreadMoudle;

int tickets = 10000;

void route(const std::string &name)
{
    while (true)
    {
        if (tickets > 0)
        {
            // 抢票过程
            usleep(1000); // 1ms -> 抢票花费的时间
            printf("who: %s, get a ticket: %d\n", name.c_str(), tickets);
            tickets--;
        }
        else
        {
            break;
        }
    }
}

int main()
{
    Thread t1("thread-1", route);
    Thread t2("thread-2", route);
    Thread t3("thread-3", route);
    Thread t4("thread-4", route);

    t1.Start();
    t2.Start();
    t3.Start();
    t4.Start();

    t1.Join();
    t2.Join();
    t3.Join();
    t4.Join();
}

【测试结果】:四个线程均抢完票,并且等待成功,回收成功;但是我们却发现一个问题,票一共只有1w张,理应最后一个线程得到的票号是1,这里确出现了负数,这是什么原因?

【解释】:计算机的运算类型有算数运算逻辑运算,并且是CPU中寄存器进行运算,在CPU内,寄存器只有一套,但是寄存器里面的数据,可以有多套;这些数据属于线程私有,看起来放在了一套公共的寄存器中,但是属于线程私有,当他被切换的时候,他要带走自己的数据!回来的时候会恢复;

  • 我们平常所用的一条代码,在汇编层上可能会对应很多汇编语句,比如一个简单的 tickets-- ,就牵涉到至少三条指令:1.重读数据,2.--数据,3.写回数据;

  • 因此在进入抢票的过程中,看似就几行代码,到了汇编层就是很多代码,CPU是会进行线程切换,这样就会发生数据不一至的问题,如何理解?我们看图

 

如何解决这种问题?加锁!!

2.认识锁和它的接口 

pthread_mutex_lock

pthread_mutex_lock 是一个在多线程编程中用于锁定互斥量(mutex)的函数。以下是关于 pthread_mutex_lock 的详细说明:

函数原型:该函数的原型定义如下:

参数:pthread_mutex_t是互斥锁的类型,任何时刻,只允许一个线程进行资源访问

功能描述:当调用 pthread_mutex_lock 时,它将尝试锁定 mutex 参数指向的互斥量。如果这个互斥量当前没有被锁定,它将被锁定,并且调用该函数的线程将成为互斥量的所有者,函数会立即返回。如果互斥量已经被其他线程锁定,那么调用该函数的线程将会阻塞,直到互斥量被解锁。

互斥量的状态:互斥量有两种状态:未锁定(此时不被任何线程拥有)和锁定(此时被一个线程拥有)。一个互斥量不能同时被两个不同的线程所拥有。如果一个线程尝试锁定一个已经被其他线程锁定的互斥量,它将会等待,直到那个线程解锁互斥量。

返回值:如果函数执行成功,返回值为 0。如果发生错误,例如尝试重新锁定已经被同一个线程锁定的互斥量,函数将返回一个错误码。


pthread_mutex_t

pthread_mutex_t 是 POSIX 线程(通常称为 pthreads)API 中定义的一个数据类型,用于表示互斥量(mutex)。互斥量是一种同步机制,用于防止多个线程同时访问共享资源,从而避免竞态条件。

定义

 

注意,pthread_mutex_t 是一个不透明的数据类型,其内部结构对用户是隐藏的。用户不应该尝试直接访问或修改这个结构体的内容。

初始化: 在使用 pthread_mutex_t 之前,必须对其进行初始化。互斥量可以通过以下几种方式进行初始化:

静态初始化(锁是全局的或者静态的):可以在声明时直接使用宏 PTHREAD_MUTEX_INITIALIZER 进行初始化。

动态初始化:使用 pthread_mutex_init 函数进行初始化。

 

其中 attr 是一个指向 pthread_mutexattr_t 结构的指针,该结构用于设置互斥量的属性。如果 attrNULL,则互斥量将使用默认属性。

销毁: 当不再需要互斥量时,应该使用 pthread_mutex_destroy 函数来释放它所占用的资源。 

在销毁一个互斥量之前,必须确保没有线程正在等待或持有该互斥量。

锁定与解锁

  • 使用 pthread_mutex_lock 尝试锁定互斥量。如果互斥量已被锁定,调用线程将阻塞直到互斥量被解锁。

  • 使用 pthread_mutex_trylock 尝试锁定互斥量,但不会阻塞;如果互斥量已被锁定,则立即返回一个错误码。

  • 使用 pthread_mutex_unlock 解锁互斥量。

属性: 互斥量可以有不同的属性,如类型(普通、递归、错误检查等),这些属性可以通过 pthread_mutexattr_t 结构来设置。

错误处理: 所有与互斥量相关的函数在出错时都会返回错误码,可以通过 strerror 函数或 perror 函数来获取错误信息。

pthread_mutex_lock / _unlock

 

pthread_mutex_lock 是 POSIX 线程(pthreads)库中的一个函数,用于锁定一个互斥量(mutex)。当一个线程调用 pthread_mutex_lock 尝试锁定一个互斥量时,以下情况可能会发生:

❍ 如果互斥量当前是未锁定的状态,调用线程会成功锁定该互斥量,并继续执行。

❍ 如果互斥量已经被另一个线程锁定,调用线程将会阻塞,直到该互斥量被解锁。

pthread_mutex_unlock 函数,这是一个 POSIX 线程(pthreads)库中的函数,用于解锁一个互斥量(mutex)。当一个线程完成了对临界区的访问后,它应该解锁互斥量,以便其他线程可以锁定并访问该临界区。

pthread_mutex_trylock

pthread_mutex_trylock 是 POSIX 线程(pthreads)库中的一个函数,用于尝试锁定一个互斥量(mutex),但它与 pthread_mutex_lock 的主要区别在于,如果互斥量已经被锁定,pthread_mutex_trylock 不会阻塞调用线程,而是立即返回一个错误码。

返回值:

  • 成功时(互斥量被成功锁定),返回 0。

  • 如果互斥量已经被锁定,返回 EBUSY

  • 出现其他错误时,返回其他错误编号。

使用场景:

  • 非阻塞互斥量锁定:当线程不希望因等待互斥量而阻塞时,可以使用 pthread_mutex_trylock

  • 避免死锁:通过尝试锁定互斥量,线程可以决定是否继续执行或采取其他操作,从而避免死锁。

  • 优先级继承:在某些实时系统中,为了避免优先级反转,可以使用 pthread_mutex_trylock 来尝试锁定互斥量。

注意事项

  • 错误处理:调用 pthread_mutex_trylock 时,应检查返回值,并根据返回值做出相应的处理。

  • 资源释放:如果 pthread_mutex_trylock 返回 EBUSY,线程应该释放已经持有的资源,避免资源泄露。

  • 重试策略:通常在使用 pthread_mutex_trylock 时,如果返回 EBUSY,线程可能会在一段时间后重试锁定。

学会锁的基本使用后我们就可以修改我们自己实现的多线程,并且重新进行抢票

mythread.hpp

#pragma once
#include <iostream>
#include <string>
#include <pthread.h>

namespace ThreadMoudle
{
    class ThreadData
    {
    public:
        ThreadData(const std::string &name, pthread_mutex_t *lock):_name(name), _lock(lock)
        {}
    public:
        std::string _name;
        pthread_mutex_t *_lock;
    };

    // 线程要执行的方法,后面我们随时调整
    typedef void (*func_t)(ThreadData *td); // 函数指针类型

    class Thread
    {
    public:
        void Excute()
        {
            std::cout << _name << " is running" << std::endl;
            _isrunning = true;
            _func(_td);
            _isrunning = false;
        }
    public:
        Thread(const std::string &name, func_t func, ThreadData *td):_name(name), _func(func), _td(td)
        {
            std::cout << "create " << name << " done" << std::endl;
        }
        static void *ThreadRoutine(void *args) // 新线程都会执行该方法!
        {
            Thread *self = static_cast<Thread*>(args); // 获得了当前对象
            self->Excute();
            return nullptr;
        }
        bool Start()
        {
            int n = ::pthread_create(&_tid, nullptr, ThreadRoutine, this);
            if(n != 0) return false;
            return true;
        }
        std::string Status()
        {
            if(_isrunning) return "running";
            else return "sleep";
        }
        void Stop()
        {
            if(_isrunning)
            {
                ::pthread_cancel(_tid);
                _isrunning = false;
                std::cout << _name << " Stop" << std::endl;
            }
        }
        void Join()
        {
            ::pthread_join(_tid, nullptr);
            std::cout << _name << " Joined" << std::endl;
            delete _td;
        }
        std::string Name()
        {
            return _name;
        }
        ~Thread()
        {
        }

    private:
        std::string _name;
        pthread_t _tid;
        bool _isrunning;
        func_t _func; // 线程要执行的回调函数
        ThreadData *_td;
    };
} // namespace ThreadModle

 main.cc

#include <iostream>
#include <vector>
#include <cstdio>
#include <unistd.h>
#include "mythread.hpp"

using namespace ThreadMoudle;
int tickets = 10000; // 共享资源,造成了数据不一致的问题

pthread_mutex_t gmutex = PTHREAD_MUTEX_INITIALIZER;
static int threadnum = 4;

void route(ThreadData *td)
{
    // std::cout <<td->_name << ": " << "mutex address: " << td->_lock << std::endl;
    // sleep(1);
    while (true)
    {
        pthread_mutex_lock(td->_lock);
        if (tickets > 0)
        {
            // 抢票过程
            usleep(1000); // 1ms -> 抢票花费的时间
            printf("who: %s, get a ticket: %d\n", td->_name.c_str(), tickets);
            tickets--;
            pthread_mutex_unlock(td->_lock);
        }
        else
        {
            pthread_mutex_unlock(td->_lock);
            break;
        }
    }
}

int main()
{
    pthread_mutex_t mutex;
    pthread_mutex_init(&mutex, nullptr);

    std::vector<Thread> threads;
    for (int i = 0; i < threadnum; i++)
    {
        std::string name = "thread-" + std::to_string(i + 1);
        ThreadData *td = new ThreadData(name, &mutex);
        threads.emplace_back(name, route, td);
    }

    for (auto &thread : threads)
    {
        thread.Start();
    }

    for (auto &thread : threads)
    {
        thread.Join();
    }

    pthread_mutex_destroy(&mutex);
    return 0;
}

【结果】:此时重新执行程序就不会出现数据不一致问题了

所谓的对临界资源进行保护,本质是对临界区代码进行保护

我们对所有资源进行访问,本质都是通过代码进行访问的!保护资源,本质就是把访问资源的代码保护起来

3.解决历史问题

  1. 所以,加锁的范围,粒度一定要尽量小

  2. 任何线程,要进行抢票,都得先申请锁,原则上不应该有例外

  3. 所有线程申请锁,前提是所有的线程都得看到这把锁,锁本身也是共享资源----加锁的过程,必须是原子的

  4. 原子性:要么不做,要做就做完,没有中间状态,就是原子性

  5. 如果线程申请锁失败了,我的线程要被阻塞

  6. 如果线程申请锁成功了,继续向后运行

  7. 如果线程申请锁成功了,执行临界区的代码了,执行临界区代码期间是可以发生切换的(比如时间片到了),但是即使切换了,其他线程无法进入!因为我虽然被切换了,但是我没有释放锁,我可以放心的执行完毕,没有人能打扰我

结论:所以对于其他线程,要么我没有申请锁,要么我释放了锁,对其他线程才有意义

4.原理角度理解这个锁

 

5.从实现角度理解锁

✸ 经过上面的例子,大家已经意识到单纯的i++或者++i 都不是原子的,有可能会有数据一致性问题

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

 

◉ CPU的寄存器只有一套,被所有的线程共享。但是寄存器里面的数据,属于执行流的上下文,属于执行流私有的数据

◉ CPU在执行代码的时候,一定要有对应的执行载体 -- 线程 && 进程

◉ 数据在内存中,被所有线程是共享的

结论:把数据从内存移动到CPU寄存器中,本质是把数据从共享,变成线程私有

 

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

Kafka中的Topic和Partition有什么关系?

大家好&#xff0c;我是锋哥。今天分享关于【Kafka中的Topic和Partition有什么关系&#xff1f;】面试题。希望对大家有帮助&#xff1b; Kafka中的Topic和Partition有什么关系&#xff1f; 1000道 互联网大厂Java工程师 精选面试题-Java资源分享网 在 Apache Kafka 中&#…

leetcode108:将有序数组转化为二叉搜索树

给你一个整数数组 nums &#xff0c;其中元素已经按 升序 排列&#xff0c;请你将其转换为一棵 平衡 二叉搜索树。 示例 1&#xff1a; 输入&#xff1a;nums [-10,-3,0,5,9] 输出&#xff1a;[0,-3,9,-10,null,5] 解释&#xff1a;[0,-10,5,null,-3,null,9] 也将被视为正确…

MyBatis执行一条sql语句的流程(源码解析)

提示&#xff1a;文章写完后&#xff0c;目录可以自动生成&#xff0c;如何生成可参考右边的帮助文档 MyBatis执行一条sql语句的流程&#xff08;源码解析&#xff09; MyBatis执行sql语句的流程加载配置文件加载配置文件的流程 创建sqlsessionFactory对象解析Mapper创建sqlses…

python23-常用的第三方库01:request模块-爬虫

requests 模块是 Python 中的一个第三方库&#xff0c;用于发送 HTTP 请求。 它提供了一个简单且直观的 API&#xff0c;使得发送网络请求和解析响应变得非常容易。requests 模块支持各种 HTTP 方法&#xff0c;如 GET、POST、PUT、DELETE 等&#xff0c;并且具有处理 cookies…

TTL 传输中过期问题定位

问题&#xff1a; 工作环境中有一个acap的环境&#xff0c;ac的wan口ip是192.168.186.195/24&#xff0c;ac上lan上有vlan205&#xff0c;其ip子接口地址192.168.205.1/24&#xff0c;ac采用非nat模式&#xff0c;而是路由模式&#xff0c;在上级路由器上有192.168.205.0/24指向…

前端超大缓存IndexDB、入门及实际使用

文章目录 往期回顾项目实战初始化表获取列表新增表的数据项获取详情根据ID获取详情根据其他字段获取详情 删除数据 总结 往期回顾 在之前的文章中&#xff0c;我们介绍了IndexDB vs Cookies vs Session这几个的对比&#xff0c;但是没有做实际项目的演示&#xff0c;今天我们用…

vue3学习笔记(11)-组件通信

1.props 父传子 子传夫 父传子 接收用defineProps([]) 空字符串也是假 2.自定义事件 $event:事件对象 ref定义的数据在模板里面引用的时候可以不用.value 3.子传父 宏函数 触发事件 声明事件 defineEmits() 挂载之后3s钟触发 4.命名 肉串命名 5.任意组件通信 mitt pubs…

【高阶数据结构】红黑树封装map、set

红黑树封装map、set 1.源码及框架分析2.模拟实现map和set1.支持 insert 的实现2.支持 iterator 的实现3.map支持 operator [] 的实现 3.总代码1.RBTree.h2.Myset.h3.Mymap.h4.Test.cpp 1.源码及框架分析 SGI-STL30版本源代码&#xff0c;map和set的源代码在map/set/stl_map.h/…

多模态论文笔记——Coca

大家好&#xff0c;这里是好评笔记&#xff0c;公主号&#xff1a;Goodnote&#xff0c;专栏文章私信限时Free。本文详细介绍多模态模型Coca&#xff0c;在DALLE 3中使用其作为captioner基准模型的原因和优势。 文章目录 ALBEF论文模型结构组成训练目标 CoCa​论文模型结构CoCa…

WebGL之Tree.js

tree基于WebGL的库绘制展示3D图形使用场景包括: 网页游&#xff1a;创建交互式的3D游戏&#xff0c;提供沉浸式的游戏体验。数据可视&#xff1a;将复杂的数据以3D形式展示&#xff0c;便于用户理解和分析。产品展&#xff1a;在电商网站上展示产品的3D模型&#xff0c;提供更…

基于PyQt5的UI界面开发——图像与视频的加载与显示

介绍 这里我们的主要目标是实现一个基于PyQt5和OpenCV的图像浏览和视频播放应用。用户可以选择本地的图像或视频文件夹&#xff0c;进行图像自动播放和图像切换以及视频播放和调用摄像头等操作&#xff0c;并且支持图像保存功能。项目的核心设计包括文件路径选择、图像或视频的…

数据结构与算法之动态规划: LeetCode 62. 不同路径 (Ts版)

不同路径 https://leetcode.cn/problems/unique-paths/description/ 描述 一个机器人位于一个 m x n 网格的左上角 &#xff08;起始点在下图中标记为 “Start” &#xff09;机器人每次只能向下或者向右移动一步。机器人试图达到网格的右下角&#xff08;在下图中标记为 “…

java自定义注解对枚举类型参数的校验

目录 1.前提准备条件 1.1 pom.xml文件依赖: 1.2 枚举类&#xff1a; 1.3 controller接口&#xff1a; 1.4 实体参数&#xff1a; 1.5 knife4j的配置 2.实现要求 3.实现步骤 3.1 自定义注解类&#xff1a; 3.2 使用注解&#xff1a; 3.3 添加注解校验类&#xff1a; …

Type c系列接口驱动电路·内置供电驱动电路使用USB2.0驱动电路!!!

目录 前言 Type c常见封装类型 Type c引脚功能详解 Type c常见驱动电路详解 Type c数据手册 ​​​​​​​ ​​​​​​​ 编写不易&#xff0c;仅供学习&#xff0c;请勿搬运&#xff0c;感谢理解 常见元器件驱动电路文章专栏连接 LM7805系列降压芯片驱动电路…

Mybatis 01

JDBC回顾 select 语句 "select *from student" 演示&#xff1a; 驱动包 JDBC 的操作流程&#xff1a; 1. 创建数据库连接池 DataSource 2. 通过 DataSource 获取数据库连接 Connection 3. 编写要执⾏带 ? 占位符的 SQL 语句 4. 通过 Connection 及 SQL 创建…

基础数据结构--二叉树

一、二叉树的定义 二叉树是 n( n > 0 ) 个结点组成的有限集合&#xff0c;这个集合要么是空集&#xff08;当 n 等于 0 时&#xff09;&#xff0c;要么是由一个根结点和两棵互不相交的二叉树组成。其中这两棵互不相交的二叉树被称为根结点的左子树和右子树。 如图所示&am…

协议幻变者:DeviceNet转ModbusTCP网关开启机器手臂智能新纪元

技术背景DeviceNet是一种广泛应用于工业自动化领域的现场总线标准&#xff0c;它能够实现控制器与现场设备之间的高效通信&#xff0c;常用于连接各种传感器、执行器以及其他工业设备&#xff0c;如机器人、电机驱动器等&#xff0c;具有实时性强、可靠性高的特点。而ModbusTCP…

Spring Security 3.0.2.3版本

“前言” 通过实践而发现真理&#xff0c;又通过实践而证实真理和发展真理。从感性认识而能动地发展到理性认识&#xff0c;又从理性认识而能动地指导革命实践&#xff0c;改造主观世界和客观世界。实践、认识、再实践、再认识&#xff0c;这种形式&#xff0c;循环往复以至无…

MiFlash 线刷工具下载合集

MiFlash 线刷工具下载合集 MiFlash 线刷工具下载合集 – MIUI历史版本相较于小米助手的刷机功能&#xff0c;线刷还是偏好使用 MiFlash。特点是界面简单纯粹&#xff0c;有自定义高级选项&#xff0c;可以选择刷机不上 BL 锁&#xff0c;自定义刷机脚本&#xff0c;EDL 刷机模…

Oracle 多租户架构简介

目录 零. 简介一. CDB&#xff08;Container Database&#xff0c;容器数据库&#xff09;二. PDB&#xff08;Pluggable Database&#xff0c;可插拔数据库&#xff09;三. CDB 与 PDB 的比较四. 用户的种类五. XE 与 XEPDB1 零. 简介 ⏹Oracle 多租户架构&#xff08;Multit…