深入了解线程锁的使用及锁的本质

文章目录

  • 线程锁的本质
    • 局部锁的使用
  • 锁的封装及演示
    • 线程饥饿问题
  • 线程加锁本质
  • 可重入和线程安全
  • 死锁问题

根据前面内容的概述, 上述我们已经知道了在linux下关于线程封装和线程互斥,锁的相关的概念, 下面就来介绍一下关于线程锁的一些其他概念.

线程锁的本质

当这个锁是全局的或者是静态属性时,可以使用PTHREAD_MUTEX_INITIALIZER (initializer 初始化器(初始化列表那样的东西)),这个宏来进行初始化.

局部锁的使用

局部的锁就要使用pthread_mutex_init()创建, pthread_mutex_destroy()来销毁
在这里插入图片描述
在这里插入图片描述

回调函数处:
在这里插入图片描述

锁的封装及演示

这边引入锁的封装, 将线程名称与锁进行封装的一种保护机制(lock guard):.
意义在于: 创建后再程序结束时会自动释放锁,方便使用

LockGuard.hpp定义

#pragma once
#include <iostream>
//不定义锁,默认认为外部会给我们传入锁对象
class Mutex
{
public:Mutex(pthread_mutex_t *lock):_lock(lock)//包装加锁功能可以实现启动自定义锁时自定加锁,然后对应的函数功能结束自动解锁(利用构造函数和析构函数的性质实现){}void Lock(){pthread_mutex_lock(_lock);}void Unlock(){pthread_mutex_unlock(_lock);}~Mutex(){}private:pthread_mutex_t *_lock;
};class LockGuard
{
public:LockGuard(pthread_mutex_t *lock):_mutex(lock)//_mutex是Mutex的对象,该对象调用对应的方法{_mutex.Lock();//调用Mutex类的加锁方法}~LockGuard (){_mutex.Unlock();}private:Mutex _mutex;
};

Thread.hpp 对pthread线程的封装实现

#pragma once
#include <iostream>
#include <functional>
#include <pthread.h>
#include <string>using namespace std;
template<class T>
using func_t = function<void(T)>;//std::function 是C++标准库中的一个模板类,可以包装任何可调用目标(callable target),比如函数、lambda表达式、函数对象(functor)等。
template<class T>
class Thread
{
public:Thread(const string &name, func_t<T> func, T data): _name(name), _func(func), _data(data), _tid(0), _isrunning(false){}static void *ThreadRoutine(void *args)//子线程入口,接受参数为当前对象的指针{Thread *t = static_cast<Thread*>(args);//转义为所需要的指针类型,当前的t和this一样,但是不能与库内的this进行重名t->_func(t->_data); //当前对象调用参数_func(他是一个function类创建的对象,这个类可以包装任何内容,这边包装函数,_func是这个函数模板创建的对象),接受来自Thread创建时的第三个参数//到这边是完成对整个类的包装,模板概念已经结束,具体操作回到main内查看,对应的函数执行结束后,执行exit(0)exit(0);}bool Start(){int n = pthread_create(&_tid, nullptr, ThreadRoutine, this);//创建线程,加载输出OS给的tid,默认方式创建(不设置分离状态,栈大小等),子线程入口,传入参数给子线程if(n == 0){_isrunning = true;return true;}else{return false;}}bool Join(){if(!_isrunning){return true;}int n = pthread_join(_tid, nullptr);if(n == 0){_isrunning = false;return true;}else{return false;}}~Thread(){}bool IsRunning(){return _isrunning;}private:pthread_t _tid;string _name;func_t<T> _func;T _data;bool _isrunning;
};

main.cc代码演示

#include "Thread.hpp"
#include <unistd.h>
#include "LockGuard.hpp"class ThreadData
{
public:ThreadData(string name, pthread_mutex_t *pmutex): _name(name), pmutex(pmutex){}~ThreadData(){}public:string _name;pthread_mutex_t *pmutex;
};
int numsize = 10000;string GetThreadName()
{static int num = 1;return static_cast<string>("Thread-" + to_string(num++));
}
void Print(ThreadData *td)//执行Print方法,参数是来自线程创建函数的第四个参数,这一功能也由线程创建函数实现,功不可没,十分可秒啊
{//全局内定义的参数进行--操作,验证线程互斥问题while (true){{//将临界区进行花括号包裹,代码更加明显LockGuard lockguard(td->pmutex);//利用锁保护功能模块进行加锁(启动锁)// LockGuard lockguard(&mutex);// pthread_mutex_lock(mutex);if (numsize > 0){usleep(1000);std::cout << td->_name << ", the numsize is: " << numsize << std::endl;--numsize;// pthread_mutex_unlock(mutex);}else{// pthread_mutex_unlock(mutex);break;}}//加锁, 解锁功能结束,一个线程访问临界区的操作也结束,意味着后续线程可以访问这个临界区//在运行结果时会发现,有时候会出现一个线程把所有numsize都分完了,这是因为线程执行多久是由于时间片决定,当在多线程情况下把所有任务(同一份资源)都做完的情况叫做多线程饥饿问题}
}int main()
{pthread_mutex_t mutex; // 创建锁初始化pthread_mutex_init(&mutex, nullptr);string name1 = GetThreadName();//获取线程名称ThreadData *td1 = new ThreadData(name1, &mutex); // 将锁和线程的名字的信息写入ThreadData,便于管理Thread<ThreadData *> t1(name1, Print, td1);//为线程创建进行加载对应信息string name2 = GetThreadName();ThreadData *td2 = new ThreadData(name2, &mutex);Thread<ThreadData *> t2(name2, Print, td2);string name3 = GetThreadName();ThreadData *td3 = new ThreadData(name3, &mutex);Thread<ThreadData *> t3(name3, Print, td3);string name4 = GetThreadName();ThreadData *td4 = new ThreadData(name4, &mutex);Thread<ThreadData *> t4(name4, Print, td4);string name5 = GetThreadName();ThreadData *td5 = new ThreadData(name5, &mutex);Thread<ThreadData *> t5(name5, Print, td5);t1.Start();//线程启动t2.Start();t3.Start();t4.Start();t5.Start();t1.Join();//线程等待t2.Join();t3.Join();t4.Join();t5.Join();pthread_mutex_destroy(&mutex); // 消除锁return 0;
}

基于上篇文章定义对main内的一些修改:
在这里插入图片描述

线程饥饿问题

再多线程创建后
在运行结果时会发现,有时候会出现一个线程把所有numsize都分完了,这是因为线程执行多久是由于时间片决定,当在多线程情况下把所有任务(同一份资源)都做完的情况叫做多线程饥饿问题.

要解决饥饿问题要让线程在执行时,预备一定的顺序性–这就是线程同步(下章见晓)

线程加锁本质

原子性问题在软硬件层面的体现
软件方面

线程能被调度是因为OS以一种非常快的方式来受理时钟中断,这时就会执行调度进程

硬件方面

把中断关掉,这时只执行进程,OS不会继续执行,这时不会进行调度

大部分的体系结构(像X86,AMD芯片中)会提供swap和exchange汇编级的指令,作用是把寄存器的内容和内存单元的内容进行数据交换

1.exchange eax mem_addr //将eax 和 mem_addr的内容进行交换
直接进行交换,这一个操作是原子性的
2.什么是一把锁?在代码中是创建一个变量,首先把他想象成一个变量struct {int num = 1;}
利用伪代码进行理解:在这里插入图片描述
在这里插入图片描述

关于加锁的原则: 谁加锁,谁解锁.

可重入和线程安全

可重入VS线程安全:

可重入还是不可重入描述的是函数的问题,跟线程无关,他描述的是函数的特点,无褒贬之分,函数大部分都是不可重入

线程安全,:
多个线程并发同一段代码时,不会出现不同的结果,常见对全局变量或静态变量进行操作,并且没有锁保护
的情况下,会出现该问题,它描述的是线程的特征

eg:线程访问不可重入函数是线程不安全的情况之一

线程安全的操作:

对于一个全局的变量,在开始改变完他的值之后在退出这个函数之前将值恢复成开始的值,这样来变相的达到线程安全的操作,这只是其中一个例子

可重入与线程安全是二义性

函数可重入意味着当线程进入这个函数是线程安全的
反之,当这个函数不可重入,那么就是线程不安全的

死锁问题

问题解释: 处于一组进程中的各个线程不会释放资源,但因为相互申请被其他进程所占据不会释放资源而处于一种永久等待的状态(多个执行流在一段时间内因为相互牵制不会向后推进)

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

互斥条件:一个资源只能被一个执行流使用(产生死锁的根本原因)
请求与保持条件:一个执行流因请求资源而阻塞时,对已获得的资源保持不放(把自己的锁拿的紧紧地,还伸手向别人要锁)
不剥夺条件:一个执行流已获得的资源,在未使用之前,不能被强行剥夺(锁2不能解锁1)
循环等待条件:若干执行流之间形成一种头尾相接的循环等待资源关系(互相申请对方的的锁的问题,形成了申请的循环)

当上述四个条件都成立才会产生死锁

如何避免死锁呢?

避免死锁:不用锁
但是为了保护共享资源, 提出来的使用锁
核心原理:
1.破坏4个必要条件中的一个或者多个
2.建议, 按照同样的次序进行申请锁的操作(加锁循序尽量保持一致)
尽量把锁的资源,按照申请的资源一次给申请线程了,这样不易出现错误(目前用不到)
3.避免锁未被释放的场景发生
4.资源一次性分配

注:

一个线程也能实现死锁:
比如:不下心把解锁写成了加锁,这个时候就会出错
这个时候是自己阻塞自己

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

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

相关文章

Codeforces Round #956 (Div. 2) and ByteRace 2024

A. Array Divisibility 思路: 找出特例,发现输出 1∼&#x1d45b; 符合题意。直接输出1~n即可. 代码: #include<bits/stdc.h> using namespace std; typedef long long ll; #define N 1000005 ll dp[N], w[N], v[N], h[N]; ll dis[1005][1005]; ll a, b, c, n, m, t;…

iOS 开发技巧 - 使用本地 json 文件

前言 使用本地 json 文件的场景&#xff0c;在我们开发功能的阶段&#xff0c;服务端接口字段定义好了后&#xff0c;有些接口响应很慢&#xff0c;请求到响应可能要 几十秒甚至一分钟&#xff0c;我们需要频繁调用接口来调试功能&#xff1b;还有就是调用一些我们需要付费的三…

Ubuntu20.04下修改samba用户密码

Ubuntu20.04下修改samba用户密码 在Ubuntu系统中&#xff0c;修改samba密码通常涉及到两个方面&#xff1a;更改samba用户的密码和重置samba服务的密码数据库。以下是如何进行操作的步骤&#xff1a; 1、更改samba用户密码&#xff1a; 打开终端&#xff0c;使用以下命令更改…

vue打包terser压缩去除控制台打印和断点

情况一&#xff1a; 1、vue-cli搭建 代码压缩工具terser在vue-cli里面是自动支持的&#xff0c;所以直接在vue.config.js里面加入下面配置&#xff1a; const {defineConfig} require(vue/cli-service) module.exportsdefineConfig({transpileDependencies:true,terser:{te…

看影视学英语(假如第一季第一集)

in the hour也代表一小时吗&#xff1f;等同于in an hour&#xff1f;

activemq-CVE-2022-41678

Apache ActiveMQ Jolokia 后台远程代码执行漏洞 Apache ActiveMQ在5.16.5&#xff0c;5.17.3版本及以前&#xff0c;后台Jolokia存在一处任意文件写入导致的远程代码执行漏洞。 启动环境 admin/admin 方法一&#xff1a;利用poc 这个方法受到ActiveMQ版本的限制&#xff0c;因…

Linux 创建新虚拟机的全过程图解

一、创建新虚拟机 1.选择自定义 2.直接下一步 3.选择稍后安装 4.设置虚拟机名和安装位置 5.配置处理器&#xff08;处理器数量&#xff1a;4、每个处理器的内核&#xff1a;2&#xff09; 6. 内存选择 7.网络类型 8. IO控制器类型-默认推荐 9.磁盘类型-默认推荐 10.选择虚拟磁…

JS代码动态打印404页面源码

JS代码动态打印404页面源码&#xff0c;适合做网站错误页&#xff0c;具有js动态打印效果&#xff0c;喜欢的朋友可以拿去 源码由HTMLCSSJS组成&#xff0c;记事本打开源码文件可以进行内容文字之类的修改&#xff0c;双击html文件可以本地运行效果&#xff0c;也可以上传到服务…

Linux udp编程

我最近开了几个专栏&#xff0c;诚信互三&#xff01; > |||《算法专栏》&#xff1a;&#xff1a;刷题教程来自网站《代码随想录》。||| > |||《C专栏》&#xff1a;&#xff1a;记录我学习C的经历&#xff0c;看完你一定会有收获。||| > |||《Linux专栏》&#xff1…

空间计量模型及 Stata 具体操作步骤

目录 一、引言 二、空间计量模型理论原理 空间自回归模型&#xff08;SAR&#xff09;&#xff1a; 空间误差模型&#xff08;SEM&#xff09;&#xff1a;&#xff0c; 空间杜宾模型&#xff08;SDM&#xff09;&#xff1a; 三、实证模型构建 四、数据准备 五、Stata …

14-56 剑和诗人30 - IaC、PaC 和 OaC 在云成功中的作用

介绍 随着各大企业在 2024 年加速采用云计算&#xff0c;基础设施即代码 (IaC)、策略即代码 (PaC) 和优化即代码 (OaC) 已成为成功实现云迁移、IT 现代化和业务转型的关键功能。 让我在云计划的背景下全面了解这些代码功能的当前状态。我们将研究现代云基础设施趋势、IaC、Pa…

【电路笔记】-C类放大器

C类放大器 文章目录 C类放大器1、概述2、C类放大介绍3、C类放大器的功能4、C 类放大器的效率5、C类放大器的应用:倍频器6、总结1、概述 尽管存在差异,但我们在之前有关 A 类、B 类和 AB 类放大器的文章中已经看到,这三类放大器是线性或部分线性的,因为它们在放大过程中再现…

Collection 和 Collections 的区别与用法

Collection 和 Collections 的区别与用法 1、Collection 接口1.1 主要特点1.2 常见方法 2、 Collections 工具类2.1 主要特点2.2 常见方法 3、示例代码3.1 使用 Collection 接口3.2 使用 Collections 工具类 4、总结 &#x1f496;The Begin&#x1f496;点点关注&#xff0c;收…

STM32学习历程(day6)

EXTI外部中断使用教程 首先先看下EXTI的框图 看这个框图就能知道要先初始化GPIO外设 那么和前面一样 1、先RCC使能时钟 2、配置GPIO 选择端口为输入模式&#xff0c; 3、配置AFIO&#xff0c;选择我们用的GPIO连接到后面的EXTI 4、配置EXTI&#xff0c;选择边沿触发方式…

LVS实验

LVS实验 nginx1 RS1 192.168.11.137 nginx2 RS2 192.168.11.138 test4 调度器 ens33 192.168.11.135 ens36 12.0.0.1 test2 客户端 12.0.0.10 一、test4 配置两张网卡地址信息 [roottest4 network-scripts]# cat ifcfg-ens33 TYPEEthernet BOOTPROTOstatic DEFROUTEyes DEVIC…

详解平面DP(上)

前言 其实平面DP和正常的dp没有什么本质上的区别&#xff0c;只不过是在二维的面上进行DP&#xff0c;而且&#xff0c;客观的说&#xff0c;其实和递推没有什么区别&#xff0c;不要把他想的太难了 讲解 本蒻鸡思前想后&#xff0c;好像关于平面DP的理论知识好像没有什么&a…

前后端分离系统

前后端分离是一种现代软件架构模式&#xff0c;特别适用于Web应用开发&#xff0c;它强调将用户界面&#xff08;前端&#xff09;与服务器端应用逻辑&#xff08;后端&#xff09;相分离。两者通过API接口进行数据交互。这种架构模式的主要优势在于提高开发效率、维护性和可扩…

Git命令常规操作

目录 常用操作示意图 文件的状态变化周期 1. 创建文件 2. 修改原有文件 3. 删除原有文件 没有添加到暂存区的数据直接 rm 删除即可&#xff1a; 对于添加到暂存区的数据 文件或目录&#xff1a; 4. 重命名暂存区数据 5. 查看历史记录 6. 还原历史数据 恢复过程的原…

最新深度技术Win7精简版系统:免费下载!

在Win7电脑操作中&#xff0c;用户想要给电脑安装上深度技术Win7精简版系统&#xff0c;但不知道去哪里才能找到该系统版本&#xff1f;接下来系统之家小编给大家带来了深度技术Win7系统精简版本的下载地址&#xff0c;方便大家点击下载安装。系统安装步骤已简化&#xff0c;新…

设计模式8-桥模式

设计模式8-Bridge 桥模式 由来与目的模式定义结构代码推导1. 类和接口的定义2. 平台实现3. 业务抽象4. 使用示例总结1. 类数量过多&#xff0c;复杂度高2. 代码重复3. 不符合单一职责原则4. 缺乏扩展性改进后的设计1. 抽象和实现分离&#xff08;桥接模式&#xff09;2. 抽象类…