Linux知识点 -- Linux多线程(二)

Linux知识点 – Linux多线程(二)

文章目录

  • Linux知识点 -- Linux多线程(二)
  • 一、线程互斥
    • 1.背景概念
    • 2.多线程访问同一个全局变量
    • 3.加锁保护
    • 4.问题
    • 5.锁的实现
  • 二、线程安全
    • 1.可重入与线程安全
    • 2.常见情况
    • 3.可重入与线程安全的联系
  • 三、死锁
    • 1.死锁概念
    • 2.死锁的条件
    • 3.避免死锁的方法


一、线程互斥

1.背景概念

  • 临界资源:多线程执行流共享的资源就叫做临界资源;
  • 临界区:每个线程内部,访问临界资源的代码,就叫做临界区;
  • 互斥:任何时刻,互斥保证有且只有一个执行流进入临界区, 访问临界资源,通常对临界资源起保护作用;
  • 原子性:不会被任何调度机制打断的操作,该操作只有两态,要么完成,要么未完成;

2.多线程访问同一个全局变量

下面实现一个抢票代码,多线程共同抢票,都访问同一个全局变量tickets,每次访问都 - -tickets:

#include <iostream>
#include <string>
#include <cstdio>
#include <cstring>
#include <cerrno>
#include <unistd.h>
#include <pthread.h>using namespace std;int tickets = 1000;void *getTickets(void *args)
{(void)args;while (true){if(tickets > 0){usleep(1000);printf("%p: %d\n", pthread_self(), tickets);tickets--;}else{break;}}
}int main()
{pthread_t t1, t2, t3;pthread_create(&t1, nullptr, getTickets, nullptr);pthread_create(&t2, nullptr, getTickets, nullptr);pthread_create(&t3, nullptr, getTickets, nullptr);pthread_join(t1, nullptr);pthread_join(t2, nullptr);pthread_join(t3, nullptr);return 0;
}

运行结果:
在这里插入图片描述
最终将tickets的数量减到了-1,但我们发现判断条件是tickets > 0才执行 - -操作;
并发访问的时候,导致了数据不一致的问题;

  • 解释:
    tickets - -这个操作翻译成汇编语句,一共有三步操作:
    (1)读取内存数据到cpu的寄存器中;
    (2)cpu内部进行计算 - -;
    (3)将结果写回内存中
    把数据读取到寄存器,就是将数据读取到执行流的上下文数据;

    因为这个tickets - - 的运算过程不是原子的,线程在运行的任何时候都有可能被切换出去,因此会发生以下的情况:
    当线程1执行完第二步的时候,被切走了,由线程2继续执行这个- - 操作;
    线程2执行完后将数据写回内存,当线程2一直执行一定时间后,将最后结果(5000)写入内存;
    此时切回了进程1,从第三步继续执行,将结果写入内存,内存中的结果又被写成了9999;
    这样就引发了因为切换问题导致的数据不一致;

3.加锁保护

为了解决多线程引发的数据不一致问题,可以为临界区代码加锁:

  • 锁的初始化:
    在这里插入图片描述
    自行初始化:
    在这里插入图片描述
    全局内定义的静态锁,使用宏初始化:
    在这里插入图片描述
  • 加锁与解锁:
    在这里插入图片描述

在临界区加锁:加锁的意义在于,在一个时刻,只允许一个执行流访问加锁的代码,将这段代码变成串行运行的;
任何一个时刻,只允许一个线程获得这把锁,其他线程都在等待;
直到拿到锁的线程最终释放掉,其他线程才可以拿到;
相当于加锁和解锁之间的代码只可以串行通信,其他代码都可以并行;

在这里插入图片描述

全局静态的锁:

//全局静态的锁,使用宏初始化
pthread_mutex_t mtx = PTHREAD_MUTEX_INITIALIZER;//pthread_mutex_t就是原生线程库提供的一个数据类型void *getTickets(void *args)
{(void)args;while (true){pthread_mutex_lock(&mtx);//为临界区代码加锁if(tickets > 0){usleep(1000);printf("%p: %d\n", pthread_self(), tickets);tickets--;}else{break;}}
}

解锁:
在这里插入图片描述
不能在这里解锁,因为如果线程执行完break之后,就不会执行解锁代码,而这把锁是全局的,还处于被该线程修改的状态,其他线程就无法拿到锁了;
应该在下面的地方解锁:

//全局静态的锁,使用宏初始化
pthread_mutex_t mtx = PTHREAD_MUTEX_INITIALIZER;//pthread_mutex_t就是原生线程库提供的一个数据类型void *getTickets(void *args)
{(void)args;while (true){pthread_mutex_lock(&mtx);//为临界区代码加锁if(tickets > 0){usleep(1000);printf("%p: %d\n", pthread_self(), tickets);tickets--;pthread_mutex_unlock(&mtx);//解锁}else{//如果线程加锁后直接运行到这里,在这里也可以解锁pthread_mutex_unlock(&mtx);//解锁break;}}
}

加锁和解锁之间的代码叫做临界区;

运行:
固定休眠时间可能会导致只有一个线程在跑,可以随即休眠时间;

#include <iostream>
#include <string>
#include <cstdio>
#include <cstring>
#include <cerrno>
#include <unistd.h>
#include <pthread.h>
#include <ctime>using namespace std;int tickets = 1000;//全局静态的锁,使用宏初始化
pthread_mutex_t mtx = PTHREAD_MUTEX_INITIALIZER;//pthread_mutex_t就是原生线程库提供的一个数据类型void *getTickets(void *args)
{(void)args;while (true){pthread_mutex_lock(&mtx);//为临界区代码加锁if(tickets > 0){usleep(rand()%1500);printf("%s: %d\n", (char*)args, tickets);tickets--;pthread_mutex_unlock(&mtx);//解锁}else{//如果线程加锁后直接运行到这里,在这里也可以解锁pthread_mutex_unlock(&mtx);//解锁break;}usleep(rand()%2000);}
}int main()
{srand((unsigned long)time(nullptr) ^ getpid());pthread_t t1, t2, t3;pthread_create(&t1, nullptr, getTickets, (void*)"thread one");pthread_create(&t2, nullptr, getTickets, (void*)"thread two");pthread_create(&t3, nullptr, getTickets, (void*)"thread three");pthread_join(t1, nullptr);pthread_join(t2, nullptr);pthread_join(t3, nullptr);return 0;
}

结果:
在这里插入图片描述
注:加锁的时候,一定要保证加锁的粒度越小越好,因为加锁会导致进程互斥,造成临界区代码只能串行访问,影响效率;

局部的锁:
在这里插入图片描述
第一个参数是锁的地址,第二个是锁的属性;

#include <iostream>
#include <string>
#include <cstdio>
#include <cstring>
#include <cerrno>
#include <unistd.h>
#include <pthread.h>
#include <ctime>using namespace std;int tickets = 1000;//全局静态的锁,使用宏初始化
//pthread_mutex_t mtx = PTHREAD_MUTEX_INITIALIZER;//pthread_mutex_t就是原生线程库提供的一个数据类型#define THREAD_NUM 5class ThreadData
{
public:ThreadData(const string &n, pthread_mutex_t *pm): tname(n),pmtx(pm){}public:string tname;//线程名pthread_mutex_t *pmtx;//锁
};void *getTickets(void *args)
{ThreadData* td = (ThreadData*)args;//接收对象while (true){pthread_mutex_lock(td->pmtx);//为临界区代码加锁if(tickets > 0){usleep(rand()%1500);printf("%s: %d\n", td->tname.c_str(), tickets);tickets--;pthread_mutex_unlock(td->pmtx);//解锁}else{//如果线程加锁后直接运行到这里,在这里也可以解锁pthread_mutex_unlock(td->pmtx);//解锁break;}usleep(rand()%2000);}delete td;return nullptr;
}int main()
{pthread_mutex_t mtx;pthread_mutex_init(&mtx, nullptr);//局部锁初始化srand((unsigned long)time(nullptr) ^ getpid());pthread_t t[THREAD_NUM];//多线程抢票逻辑for(int i = 0; i < THREAD_NUM; i++){string name = "thread ";name += to_string(i + 1);ThreadData *td = new ThreadData(name, &mtx);//创建对象pthread_create(t + i, nullptr, getTickets, (void*)td);//创建线程的时候,穿的参数也可以是对象指针}for(int i = 0; i < THREAD_NUM; i++){pthread_join(t[i], nullptr);}pthread_mutex_destroy(&mtx);//局部锁的销毁return 0;
}

在上面的代码中,创建了一个类保存线程的信息和锁的指针,创建线程的时候,给回调函数传的参数也可以传这个对象,这样就把线程属性和局部锁的指针都传进去了,在回调函数中就可以进行使用;

运行结果:
在这里插入图片描述

4.问题

  • 加锁之后,线程在临界区中是否会切换?
    会被切换,但是不会出问题;因为该线程是持有锁被切换的,所以其他抢票线程要执行临界区代码,也必须先申请锁,但是锁已经被该线程申请了,其他线程就无法申请成功,因此,就不会让其他线程进入临界区,保证了临界区中数据的一致性;

  • 一个线程,不申请锁,就是单纯的访问临界资源,这是错误的编码方式;

  • 当一个线程持有锁,在其他线程看来,该线程就是原子的;

  • 所本身就是一种共享资源,那么谁来保证锁的安全呢?
    为了保证锁的安全,申请和释放锁,必须是原子的;

5.锁的实现

  • exchange或swap汇编指令:
    以一条汇编指令的方式,将内存和CPU寄存器的数据进行交换;站在汇编的角度,只有一条汇编语句,就认为该语句的执行是原子的;

  • 在执行流视角,是如何看待COU上面的寄存器的?
    CPU内部的寄存器,本质叫做当前执行流的上下文,这些寄存器的空间是被所有执行流共享的,但是寄存器的内容,是被每一个执行流私有的,当执行流切换的时候,会将寄存器内的数据(上下文数据)一并带走;

  • 加锁和解锁的汇编代码:(伪代码)
    在这里插入图片描述
    核心的语句就是下面这句:
    在这里插入图片描述
    将寄存器的内容和锁的内容交换,这是一行汇编代码,是原子的;
    多线程申请锁的可能的情况:
    在这里插入图片描述
    内存mutex中的1只能被一个线程交换,如果A线程已经执行了这一条指令,将al寄存器的值(0)和mutex的值(1)交换;
    交换完成后,mutex的值就变为了0,相当于锁已经被A线程拿走了,此时线程A被切换了,连带着寄存器al中的值一起带走;
    当另一个线程B来的时候,内存mutex中这个1已经被上一个线程交换了;
    现在mutex中的值是0,第二个线程交换完后将0交换到了寄存器al中,因此只能等待;

    释放锁就是再将线程寄存器al的内容和内存mutex的内容再交换回来;

二、线程安全

1.可重入与线程安全

  • 可重入:
    在这里插入图片描述
    可重入是针对函数而言的;
  • 线程安全:
    在这里插入图片描述
    线程安全是用来描述线程的;

2.常见情况

  • 线程不安全:
    在这里插入图片描述

  • 线程安全:
    在这里插入图片描述

  • 不可重入:
    在这里插入图片描述

  • 可重入:
    在这里插入图片描述

3.可重入与线程安全的联系

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

三、死锁

1.死锁概念

死锁:是指再一组线程中的各个线程均占有不会释放的资源,但因互相申请被其他进程所占的资源而初一的一种永久等待的状态;

  • 两个线程同时申请对方已有的锁,形成互相申请对方资源的一种环路情况;

同一个线程反复申请同一把锁,也会造成死锁:

#include <iostream>
#include <string>
#include <cstdio>
#include <cstring>
#include <cerrno>
#include <unistd.h>
#include <pthread.h>
#include <ctime>
#include<cassert>using namespace std;int tickets = 1000;#define THREAD_NUM 5class ThreadData
{
public:ThreadData(const string &n, pthread_mutex_t *pm): tname(n),pmtx(pm){}public:string tname;//线程名pthread_mutex_t *pmtx;//锁
};void *getTickets(void *args)
{ThreadData* td = (ThreadData*)args;//接收对象while (true){int n = pthread_mutex_lock(td->pmtx);//为临界区代码加锁assert(n == 0);if(tickets > 0){usleep(rand()%1500);printf("%s: %d\n", td->tname.c_str(), tickets);tickets--;int n = pthread_mutex_lock(td->pmtx);//听一个进程反复申请同一把锁,也会造成死锁assert(n == 0);}else{int n = pthread_mutex_lock(td->pmtx);assert(n == 0);break;}usleep(rand()%2000);}delete td;return nullptr;
}int main()
{pthread_mutex_t mtx;pthread_mutex_init(&mtx, nullptr);//局部锁初始化srand((unsigned long)time(nullptr) ^ getpid());pthread_t t[THREAD_NUM];//多线程抢票逻辑for(int i = 0; i < THREAD_NUM; i++){string name = "thread ";name += to_string(i + 1);ThreadData *td = new ThreadData(name, &mtx);//创建对象pthread_create(t + i, nullptr, getTickets, (void*)td);//创建线程的时候,穿的参数也可以是对象指针}for(int i = 0; i < THREAD_NUM; i++){pthread_join(t[i], nullptr);}pthread_mutex_destroy(&mtx);//局部锁的销毁return 0;
}

运行结果:
在这里插入图片描述
该线程运行了一次就卡住不动了,进入了死锁状态;

2.死锁的条件

  • 死锁的四个必要条件(全部满足即造成死锁):
    (1)互斥条件:一个资源每次只能被一个执行流使用;
    (2)请求与保持条件:一个执行流因请求资源而阻塞时,对已获得的资源保持不放;
    (3)不剥夺条件:一个执行流已获得的资源,在末使用完之前,不能强行剥夺;
    (4)循环等待条件:若干执行流之间形成一种头尾相接的循环等待资源的关系;

3.避免死锁的方法

(1)破坏死锁的四个必要条件的其中一个;

  • 互斥:可不可以不加锁;
  • 请求与保持:申请锁时可以使用trylock,如果锁被占有,就返回错误码,连续申请若干次,不成功,就把自己的锁释放掉,不会导致线程阻塞;

(2)加锁顺序一致;
(3)避免锁未释放的场景;
(4)资源一次性分配;

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

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

相关文章

excel文本函数篇2

本期主要介绍LEN、FIND、SEARCH以及后面加B的情况&#xff1a; &#xff08;1&#xff09;后缀没有B&#xff1a;一个字节代表一个中文字符 &#xff08;2&#xff09;后缀有B&#xff1a;两个字节代表一个中文字符 1、LEN(text)&#xff1a;返回文本字符串中的字符个数 2、…

七夕给TA满分宠爱!浪漫攻略为约会加分

浪漫的七夕将至&#xff0c;无论是异地恋人还是约会情侣&#xff0c;怎么能缺少节日仪式感~精心策划的约会计划&#xff0c;让浪漫“超级加倍”。 美好的二人世界&#xff0c;共度甜蜜时光&#xff0c;当然需要提前做好攻略&#xff0c;风和日丽的好天气能为约会加分不少。在规…

Ubuntu软件源、pip源大全,国内网站网址,阿里云、网易163、搜狐、华为、清华、北大、中科大、上交、山大、吉大、哈工大、兰大、北理、浙大

文章目录 一、企业镜像源1、阿里云2、网易1633、搜狐镜像4、华为 二&#xff1a;高校镜像源1、清华源2、北京大学3、中国科学技术大学源 &#xff08;USTC&#xff09;4、 上海交通大学5、山东大学6、 吉林大学开源镜像站7、 哈尔滨工业大学开源镜像站8、 西安交通大学软件镜像…

java网络编程

目录 1. 什么是网络编程? 2. 网络编程三要素 2.1 IP 2.1.1 常见CMD命令 2.1.2 InetAddress 2.2 端口号 2.3 协议 3. UDP通信程序 3.1 UDP的三种通信方式 4. TCP通信程序 4.1 三次握手四次挥手 1. 什么是网络编程? 在网络通信协议下&#xff0c;不同计算机上运行的程…

如何在前端实现WebSocket发送和接收TCP消息(多线程模式)

目录 第一步&#xff1a;创建WebSocket连接第二步&#xff1a;监听WebSocket事件第三步&#xff1a;发送消息第四步&#xff1a;后端处理函数说明 当在前端实现WebSocket发送和接收TCP消息时&#xff0c;可以使用以下步骤来实现多线程模式。本文将详细介绍如何在前端实现WebSoc…

抖音短视频SEO矩阵系统源码开发

一、概述 抖音短视频SEO矩阵系统源码是一项综合技术&#xff0c;旨在帮助用户在抖音平台上创建并优化短视频内容。本文将详细介绍该系统的技术架构、核心代码、实现过程以及优化建议&#xff0c;以便读者更好地理解并应用这项技术。 二、技术架构 抖音短视频SEO矩阵系统采用前…

情人节特别定制:多种语言编写动态爱心网页(附完整代码)

写在前面案例1&#xff1a;HTML Three.js库案例2&#xff1a;HTML CSS JavaScript案例3&#xff1a;Python环境 Flask框架结语 写在前面 随着七夕节的临近&#xff0c;许多人都在寻找独特而令人难忘的方式来表达爱意。在这个数字时代&#xff0c;结合创意和技术&#xff0…

多维时序 | MATLAB实现WOA-CNN-GRU-Attention多变量时间序列预测

多维时序 | MATLAB实现WOA-CNN-GRU-Attention多变量时间序列预测 目录 多维时序 | MATLAB实现WOA-CNN-GRU-Attention多变量时间序列预测预测效果基本介绍模型描述程序设计参考资料 预测效果 基本介绍 MATLAB实现WOA-CNN-GRU-Attention多变量时间序列预测&#xff0c;WOA-CNN-GR…

【JavaEE基础学习打卡04】JDBC之MySQL数据库安装

目录 前言一、JDBC与数据库二、MySQL数据库1.MySQL数据库2.MySQL服务下载安装3.MySQL服务启动停止4.MySQL命令 三、MySQL客户端安装总结 前言 &#x1f4dc; 本系列教程适用于JavaWeb初学者、爱好者&#xff0c;小白白。我们的天赋并不高&#xff0c;可贵在努力&#xff0c;坚持…

EmbedPress Pro 在WordPress网站中嵌入任何内容

EmbedPress Pro可让您通过高级自定义、自定义品牌、延迟加载和更多惊人功能嵌入源。为古腾堡块和Elementor编辑器提供支持的一体化 WordPress 嵌入解决方案。使用 EmbedPress 在古腾堡创建交互式内容。使用 EmbedPress 的古腾堡块立即将任何内容嵌入到您的网站。 网址: EmbedP…

CentOS下载ISO镜像的方法

目录 一、CentOS 介绍 二、进入CentOS 官方网站 三、步骤 一、CentOS 介绍 CentOS&#xff0c;中文意思是社区企业操作系统是Linux发行版之一&#xff0c;是免费的、开源的、可以重新分发的开源操作系统。 CentOS Linux发行版是一个稳定的&#xff0c;可预测的&#xff0…

API自动化管理: 从繁琐到轻松

在数字化时代&#xff0c;API&#xff08;应用程序编程接口&#xff09;在软件开发中扮演着至关重要的角色。然而&#xff0c;API管理可能会变得十分繁琐&#xff0c;耗费大量时间和资源。那么&#xff0c;如何实现API自动化管理&#xff0c;从而节省时间、提高效率&#xff0c…

海外ios应用商店优化排名因素之应用名称

当我们的应用出现在搜索结果中时&#xff0c;用户会更详细地查看并转到我们的应用程序页面&#xff0c;引入页面视图&#xff0c;点击下载应用&#xff0c;或者是直接忽略。所以在获得曝光度之后如何决定完全取决于优化因素&#xff0c;例如应用图标、屏幕截图和视频预览以及其…

Python+request+unittest实现接口测试框架集成实例

这篇文章主要介绍了Pythonrequestunittest实现接口测试框架集成实例&#xff0c;小编觉得挺不错的&#xff0c;现在分享给大家&#xff0c;也给大家做个参考。一起跟随小编过来看看吧 1、为什么要写代码实现接口自动化 大家知道很多接口测试工具可以实现对接口的测试&#xf…

JVM——垃圾回收器G1+垃圾回收调优

4.4 G1&#xff08;一个垃圾回收器&#xff09; 定义: 取代了CMS垃圾回收器。和CMS一样时并发的。 适用场景: 物理上分区&#xff0c;逻辑上分代。 相关JVM参数: -XX:UseG1GC-XX:G1HeapRegionSizesize-XX:MaxGCPauseMillistime 1) G1 垃圾回收阶段 三个回收阶段&#xff0…

Pydev·离线git包

Pydev离线git包 1.下载离线git包&#xff1a;eclipse.egit.repository-4.4.0.201606070830-r.zip 2.将解压后目录&#xff1a;eclipse.egit.repository-4.4.0.201606070830-r\plugins下的jar文件放到 ide\eclipse\plugins目录下 3.重启pydevIDE 百度搜索站长工具&#xff1a;h…

不负众望~历时4年修炼,这本册子终于成书了(文末赠书)

名字&#xff1a;阿玥的小东东 学习&#xff1a;Python、C/C 主页链接&#xff1a;阿玥的小东东的博客_CSDN博客-python&&c高级知识,过年必备,C/C知识讲解领域博主 目录 精进Spring Boot首选读物 “小册”变“大书”&#xff0c;彻底弄懂Spring Boot 全方位配套资源…

PHP“牵手”淘宝商品评论数据采集方法,淘宝API接口申请指南

淘宝天猫商品评论数据接口 API 是开放平台提供的一种 API 接口&#xff0c;它可以帮助开发者获取商品的详细信息&#xff0c;包括商品的标题、描述、图片等信息。在电商平台的开发中&#xff0c;详情接口API是非常常用的 API&#xff0c;因此本文将详细介绍详情接口 API 的使用…

Linux journalctl命令详解(journalctl指令)(systemd服务默认日志管理工具)

文章目录 Linux Journalctl命令详解1. Journalctl简介2. Journalctl基础使用3. 过滤日志条目4. 时间戳和日志轮转5. 高级应用6. journalctl --help指令文档英文中文 注意事项journal日志不会将程序输出的空行显示&#xff0c;日志会被压缩得满满当当。journal日志不会自动持久化…