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 5

class 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 5

class 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)资源一次性分配;

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

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

相关文章

Azure CLI 进行磁盘加密

什么是磁盘加密 磁盘加密是指在Azure中对虚拟机的磁盘进行加密保护的一种机制。它使用Azure Key Vault来保护磁盘上的数据&#xff0c;以防止未经授权的访问和数据泄露。使用磁盘加密&#xff0c;可以保护磁盘上的数据以满足安全和合规性要求。 参考文档&#xff1a;https://l…

基于swing的超市管理系统java仓库库存进销存jsp源代码mysql

本项目为前几天收费帮学妹做的一个项目&#xff0c;Java EE JSP项目&#xff0c;在工作环境中基本使用不到&#xff0c;但是很多学校把这个当作编程入门的项目来做&#xff0c;故分享出本项目供初学者参考。 一、项目描述 基于swing的超市管理系统 系统有3权限&#xff1a;管…

RHCE——四、Web服务器(理论篇)

Web服务器 一、Web服务器1、www简介1.1 常见Web服务程序介绍&#xff1a;1.2 服务器主机1.3 主要数据1.4 浏览器 2、网址及HTTP简介2.1 URL2.2 http请求方法:2.3 HTTP协议请求的工作流程&#xff1a; 3、www服务器的类型静态网站动态网站 二、快速安装Apache1、安装2、准备工作…

kafka--kafka的基本概念-topic和partition

一、kafka的基本概念-topic和partition 1、topic &#xff08;主题 &#xff09; topic是逻辑概念 以Topic机制来对消息进行分类的&#xff0c;同一类消息属于同一个Topic&#xff0c;你可以将每个topic看成是一个消息队列。 生产者&#xff08;producer&#xff09;将消息发…

Android动态添加和删除控件/布局

一、引言 最近在研究RecyclerView二级列表的使用方法&#xff0c;需要实现的效果如下。 然后查了一些博客&#xff0c;觉得实现方式太过复杂&#xff0c;而且这种方式也不是特别受推荐&#xff0c;所以请教了别人&#xff0c;得到了一种感觉还不错的实现方式。实现的思路为&…

MySQL分页查询-性能优化

MySQL分页查询优化 一、背景二、原因三、解决四、原理探究 https://blog.csdn.net/hollis_chuang/article/details/130570281 一、背景 业务背景&#xff1a;给C端10万级别的用户&#xff0c;同时发送活动消息&#xff0c;活动消息分为6类。数据背景&#xff1a;mysql表有百万…

SpringBoot自动配置原理

Spring Boot 的自动配置可以根据添加的jar依赖&#xff0c;自动配置 Spring Boot 应用程序。例如&#xff0c;我们想要使用Redis&#xff0c;直接在POM文件中增加spring-boot-starter-data-redis依赖&#xff0c;然后我们配置下连接信息就可以使用了。 那么Spring Boot 是如何…

机器学习笔记之优化算法(十五)Baillon Haddad Theorem简单认识

机器学习笔记之优化算法——Baillon Haddad Theorem简单认识 引言 Baillon Haddad Theorem \text{Baillon Haddad Theorem} Baillon Haddad Theorem简单认识证明过程证明&#xff1a;条件 1 ⇒ 1 \Rightarrow 1⇒ 条件 2 2 2证明&#xff1a;条件 3 ⇒ 3 \Rightarrow 3⇒条件 1…

一个计算机专业的学生数据结构这门课学到什么程度才能算学的还不错?

数据结构之所以重要是因为它处于算法中的基础地位&#xff0c;与解决实际问题关系密切&#xff1b;而之所以不重要是因为课本上能学到的所有实现都已经有人造过轮子了&#xff0c;甚至已经作为很多语言的标准API存在了。 换句话来说&#xff0c;在以后的编码生涯中&#xff0c…

ceph集群的扩容缩容

文章目录 集群扩容添加osd使用ceph-deploy工具手动添加 添加节点新节点前期准备新节点安装ceph&#xff0c;出现版本冲突 ceph-deploy增加节点 集群缩容删除osd删除节点 添加monitor节点删除monitor节点使用ceph-deploy卸载集群 实验所用虚拟机均为Centos 7.6系统&#xff0c;8…

Leetcode74. 搜索二维矩阵

给你一个满足下述两条属性的 m x n 整数矩阵&#xff1a; 每行中的整数从左到右按非递减顺序排列。每行的第一个整数大于前一行的最后一个整数。 给你一个整数 target &#xff0c;如果 target 在矩阵中&#xff0c;返回 true &#xff1b;否则&#xff0c;返回 false 。 class…

【从零开始的rust web开发之路 二】axum中间件和共享状态使用

系列文章目录 第一章 axum学习使用 第二章 axum中间件使用 文章目录 系列文章目录前言一、中间件是什么二、中间件使用常用中间件使用中间件使用TraceLayer中间件实现请求日志打印自定义中间件 共享状态 前言 上篇文件讲了路由和参数相应相关的。axum还有个关键的地方是中间件…

防火墙+路由模式部署

一、防火墙 防火墙最主要功能是提供访问控制能力 防火墙默认管理口为ge0/0&#xff08;部分型号有专门的MGT口&#xff09;&#xff0c;管理地址为https://192.168.1.250&#xff0c;默认管理口只开启了https和ping。登录防火墙串口&#xff0c;波特率为9600&#xff0c;默认…

【校招VIP】测试专业课之TCP/IP模型

考点介绍&#xff1a; 大厂测试校招面试里经常会出现TCP/IP模型的考察&#xff0c;TCP/IP协议是网络基础知识&#xff0c;但是在校招面试中很多同学在基础回答中不到位&#xff0c;或者倒在引申问题里&#xff0c;就丢分了。 『测试专业课之TCP/IP模型』相关题目及解析内容可点…

uniapp-滑块验证组件wo-slider

wo-slider是一款支持高度自定义的滑块验证组件&#xff0c;采用uniapp-vue2编写 采用touchstart、touchmove、touchend事件实现的滑块组件,支持H5、微信小程序&#xff08;其他小程序未试过&#xff0c;可自行尝试&#xff09; 可到插件市场下载尝试&#xff1a; https://ext.…

算法通关村十二关 | 字符串转换

1. 转换小写字母 LeetCode709&#xff1a;给你一个字符串s&#xff0c;将该字符串中的大写字母转换成相同的小写字母&#xff0c;返回新的字符串。 每个字母都是有确定的ASCII的&#xff0c;可以根据码表操作子字符串&#xff0c;常见的ASCII范围是&#xff1a; a-z: 97-122, …

无涯教程-Perl - warn函数

描述 此函数将LIST的值打印到STDERR。基本上与die函数相同,除了不对出口进行任何调用并且在eval语句内不引发异常。这对于引发错误而不导致脚本过早终止很有用。 如果变量$包含一个值(来自先前的eval调用),并且LIST为空,则$的值将以。\t.caught打印。附加到末尾。如果$和LIST…

聚观早报|京东称在技术投入没有止境;木蚁机器人完成B2轮融资

【聚观365】8月18日消息 京东零售CEO表示在技术上投入没有止境 木蚁机器人完成B2轮超亿元融资 耐能推出AI芯片KL730 三星电子泰勒晶圆厂首家客户是AI半导体厂商 韩国新能源汽车7月出口额同比大增36% 京东零售CEO表示在技术上投入没有止境 近日&#xff0c;京东零售CEO辛利…

Python开发环境(Visual Studio Code、Anaconda、PyInstaller、Enigma Virtual Box)

Python开发环境 [Anaconda、PyInstaller、Enigma Virtual Box] AnacondaAnaconda安装搭建Python环境Anaconda命令 Visual Studio CodeVisual Studio Code中Python设置Visual Studio Code中安装PyQt5Visual Studio Code中使用Qt DesignerVisual Studio Code中Anaconda切换虚拟环…

了解 JSON 格式

一、JSON 基础 JSON&#xff08;JavaScript Object Notation&#xff0c;JavaScript 对象表示法&#xff09;是一种轻量级的数据交换格式&#xff0c;JSON 的设计目的是使得数据的存储和交换变得简单。 JSON 易于人的阅读和书写&#xff0c;同时也易于机器的解析和生成。尽管 J…