当前位置: 首页 > news >正文

【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寄存器中,本质是把数据从共享,变成线程私有

 

相关文章:

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

一、线程互斥 我们把多个线程能够看到的资源叫做共享资源&#xff0c;我们对共享资源进行保护&#xff0c;就是互斥 1.多线程访问问题 【示例】见一见多线程访问问题&#xff0c;下面是一个抢票的代码&#xff0c;共计票数10000张&#xff0c;4个线程去抢 之前我们展示过封…...

[江科大STM32] 第五集快速建立STM32工程模板——笔记

保存&#xff0c;进去选芯片型号&#xff0c;我们是F10C8T6 一个MD&#xff0c;还有所有.c.h 这里所有文件 这里所有文件...

流水线并行举例说明;GPU 的细粒度问题

GPU 的细粒度与模型并行和流水线并行关系 使用模型并行和流水线并行之后会涉及到一个模型切分细粒度的问题,先切分多头(并行执行),每一个多头在切分不同阶段(串行执行)。这种情况下GPU的细粒度是多少 在这种模型并行和流水线并行结合且按多头和阶段切分的情况下,GPU 的…...

如何确保Kafka集群的高可用?

大家好&#xff0c;我是锋哥。今天分享关于【如何确保Kafka集群的高可用&#xff1f;】面试题。希望对大家有帮助&#xff1b; 如何确保Kafka集群的高可用&#xff1f; 1000道 互联网大厂Java工程师 精选面试题-Java资源分享网 要确保 Kafka 集群 的高可用性&#xff0c;需要…...

计算机毕业设计Python+Spark考研预测系统 考研推荐系统 考研数据分析 考研大数据 大数据毕业设计 大数据毕设

温馨提示&#xff1a;文末有 CSDN 平台官方提供的学长联系方式的名片&#xff01; 温馨提示&#xff1a;文末有 CSDN 平台官方提供的学长联系方式的名片&#xff01; 温馨提示&#xff1a;文末有 CSDN 平台官方提供的学长联系方式的名片&#xff01; 作者简介&#xff1a;Java领…...

Oracle SqlPlus常用命令简介

参考资料 【SQL*Plus】SETシステム変数の設定前後の具体例 目录 一. 执行系命令1.1 执行系统命令1.2 执行sql脚本文件1.2.1 在数据库中执行sql脚本1.2.2 通过sqlplus执行sql脚本 二. show命令2.1 显示SqlPlus中的全部环境变量2.2 显示指定环境变量的设置 三. 时间显示3.1 set …...

8.若依系统监控与定时任务

帮助开发者和运维快速了解应用程序的性能状态。 数据监控 定时任务 实现动态管理任务。 需求&#xff1a;每间隔5s&#xff0c;控制台输出系统时间。 新建的任务类必须在指定目录ruoyi-quartz模块下的task包下。 状态设置为启动 执行策略 场景&#xff1a;比如一个任务每个…...

《计算机组成及汇编语言原理》阅读笔记:p160-p176

《计算机组成及汇编语言原理》学习第 12 天&#xff0c;p160-p176 总结&#xff0c;总计 17 页。 一、技术总结 1.PowerPC (1)programming model(mode) As in most modern computers, there are at least two separate views of the system (formally called programming m…...

TCP网络编程(三)—— 客户端的编写/服务器端和客户端的通信

上篇文章我们学习了TCP的服务器端模式的编写&#xff0c;这篇文章我们将开始编写客户端的代码&#xff0c;完成服务器端和客户端的通信。完整代码和演示在文章的后面。 和服务器端不同&#xff0c;在客户端我们只需要服务器端的套接字和服务器端的地址和端口&#xff0c;用于向…...

如何在谷歌浏览器中使用自定义模板

作为最常用的网络浏览器之一&#xff0c;谷歌浏览器不仅提供了强大的功能&#xff0c;还允许用户通过各种方式自定义其外观和功能。其中&#xff0c;使用自定义模板可以极大地提升用户体验&#xff0c;无论是更改浏览器的外观还是优化网页显示效果。本文将详细介绍如何在谷歌浏…...

Day2 微服务 网关路由转发、网关登录校验、配置管理

目录 1.网关路由转发 1.1 网关 1.2 快速入门 1.2.1 创建项目 1.2.2 引入依赖 1.2.3 启动类 1.2.4 配置路由 1.2.5 测试 1.3 路由过滤 2.网关登录校验 2.1 鉴权思路分析 2.2 网关过滤器 2.3 自定义过滤器 2.3.1 自定义GatewayFilter 2.3.2 自定义GlobalFilter 2.4 登录校验 2.4.…...

Android 旋转盘导航栏

1.直接上源码&#xff1a; package com.you.arc;import android.content.Context; import android.graphics.Canvas; import android.graphics.Color; import android.graphics.Paint; import android.graphics.Point; import android.graphics.RectF; import android.support…...

空域降噪、频域降噪和时域降噪

目录 算法原理&#xff1a; 1.图像噪声 2.图像中常见的噪声的类型 3.不同域的定义 4.空域降噪 4.1.空域降噪的定义&#xff1a; 4.2.思想核心&#xff1a; 4.3.局部的线性算法 高斯降噪 4.4.非局部算法 5.频域降噪 傅里叶降噪&#xff1a; 小波降噪&#xff1a; …...

Cornerstone3D:了解Nifti文件,并查看元数据

Nifti 全称Neuroimaging Informatics Technology Initiative是一种专为存储医学和神经影像数据而设计的文件格式。设计目的是高效的存储三维或四维图像数据&#xff0c;同时将相关的元数据紧凑地嵌入文件中。Nifti文件的组成&#xff1a;头信息&#xff08;元数据&#xff09;…...

设计模式の状态策略责任链模式

文章目录 前言一、状态模式二、策略模式三、责任链模式 前言 本篇是关于设计模式中的状态模式、策略模式、以及责任链模式的学习笔记。 一、状态模式 状态模式是一种行为设计模式&#xff0c;核心思想在于&#xff0c;使某个对象在其内部状态改变时&#xff0c;改变该对象的行为…...

DevOps流程CICD之Jenkins使用操作

一、jenkins的docker-compose安装部署 请参考 jenkins的docker安装部署配置全网最详细教程-CSDN博客 二、创建repository 三、创建ssh 四、创建视图 五、创建任务 六、配置gitlab钩子 七、自动构建部署CI/CD验证...

【玩转23种Java设计模式】行为型模式篇:备忘录模式

软件设计模式&#xff08;Design pattern&#xff09;&#xff0c;又称设计模式&#xff0c;是一套被反复使用、多数人知晓的、经过分类编目的、代码设计经验的总结。使用设计模式是为了可重用代码、让代码更容易被他人理解、保证代码可靠性、程序的重用性。 汇总目录链接&…...

Unity Shader TexelSize的意义

TexelSize在制作玻璃折射效果时会用到。 // Get the normal in tangent space fixed3 bump UnpackNormal(tex2D(_BumpMap, i.uv.zw)); // Compute the offset in tangent space float2 offset bump.xy * _Distortion * _RefractionTex_TexelSize.xy; i.scrPos.xy offset * i…...

三、STM32MP257系列之定制Yocto Machine

文章目录 STM32MP257系列之定制的Yocto Machine1. TFA 定制2. OPTEE OS定制3. Uboot 定制3.1 创建 board3.2 创建 board的头文件3.3 创建 board的配置文件3.4 添加我们自己的dtb文件3.5 生成新patch打包到uboot recipe中3.6 修改yocto中的配置 4. Kernel 定制4.1 定制设备树 5.…...

小程序信息收集(小迪网络安全笔记~

免责声明&#xff1a;本文章仅用于交流学习&#xff0c;因文章内容而产生的任何违法&未授权行为&#xff0c;与文章作者无关&#xff01;&#xff01;&#xff01; 附&#xff1a;完整笔记目录~ ps&#xff1a;本人小白&#xff0c;笔记均在个人理解基础上整理&#xff0c;…...

脑机新手指南(八):OpenBCI_GUI:从环境搭建到数据可视化(下)

一、数据处理与分析实战 &#xff08;一&#xff09;实时滤波与参数调整 基础滤波操作 60Hz 工频滤波&#xff1a;勾选界面右侧 “60Hz” 复选框&#xff0c;可有效抑制电网干扰&#xff08;适用于北美地区&#xff0c;欧洲用户可调整为 50Hz&#xff09;。 平滑处理&…...

React Native 开发环境搭建(全平台详解)

React Native 开发环境搭建&#xff08;全平台详解&#xff09; 在开始使用 React Native 开发移动应用之前&#xff0c;正确设置开发环境是至关重要的一步。本文将为你提供一份全面的指南&#xff0c;涵盖 macOS 和 Windows 平台的配置步骤&#xff0c;如何在 Android 和 iOS…...

无法与IP建立连接,未能下载VSCode服务器

如题&#xff0c;在远程连接服务器的时候突然遇到了这个提示。 查阅了一圈&#xff0c;发现是VSCode版本自动更新惹的祸&#xff01;&#xff01;&#xff01; 在VSCode的帮助->关于这里发现前几天VSCode自动更新了&#xff0c;我的版本号变成了1.100.3 才导致了远程连接出…...

华为OD机试-食堂供餐-二分法

import java.util.Arrays; import java.util.Scanner;public class DemoTest3 {public static void main(String[] args) {Scanner in new Scanner(System.in);// 注意 hasNext 和 hasNextLine 的区别while (in.hasNextLine()) { // 注意 while 处理多个 caseint a in.nextIn…...

Android Bitmap治理全解析:从加载优化到泄漏防控的全生命周期管理

引言 Bitmap&#xff08;位图&#xff09;是Android应用内存占用的“头号杀手”。一张1080P&#xff08;1920x1080&#xff09;的图片以ARGB_8888格式加载时&#xff0c;内存占用高达8MB&#xff08;192010804字节&#xff09;。据统计&#xff0c;超过60%的应用OOM崩溃与Bitm…...

Docker 本地安装 mysql 数据库

Docker: Accelerated Container Application Development 下载对应操作系统版本的 docker &#xff1b;并安装。 基础操作不再赘述。 打开 macOS 终端&#xff0c;开始 docker 安装mysql之旅 第一步 docker search mysql 》〉docker search mysql NAME DE…...

Spring AOP代理对象生成原理

代理对象生成的关键类是【AnnotationAwareAspectJAutoProxyCreator】&#xff0c;这个类继承了【BeanPostProcessor】是一个后置处理器 在bean对象生命周期中初始化时执行【org.springframework.beans.factory.config.BeanPostProcessor#postProcessAfterInitialization】方法时…...

[特殊字符] 手撸 Redis 互斥锁那些坑

&#x1f4d6; 手撸 Redis 互斥锁那些坑 最近搞业务遇到高并发下同一个 key 的互斥操作&#xff0c;想实现分布式环境下的互斥锁。于是私下顺手手撸了个基于 Redis 的简单互斥锁&#xff0c;也顺便跟 Redisson 的 RLock 机制对比了下&#xff0c;记录一波&#xff0c;别踩我踩过…...

TCP/IP 网络编程 | 服务端 客户端的封装

设计模式 文章目录 设计模式一、socket.h 接口&#xff08;interface&#xff09;二、socket.cpp 实现&#xff08;implementation&#xff09;三、server.cpp 使用封装&#xff08;main 函数&#xff09;四、client.cpp 使用封装&#xff08;main 函数&#xff09;五、退出方法…...

k8s从入门到放弃之Pod的容器探针检测

k8s从入门到放弃之Pod的容器探针检测 在Kubernetes&#xff08;简称K8s&#xff09;中&#xff0c;容器探测是指kubelet对容器执行定期诊断的过程&#xff0c;以确保容器中的应用程序处于预期的状态。这些探测是保障应用健康和高可用性的重要机制。Kubernetes提供了两种种类型…...