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

news2024/9/24 9:25:10

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

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

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

相关文章

【力扣每日一题】2023.8.17 切披萨的方案数

目录 题目&#xff1a; 示例&#xff1a; 分析&#xff1a; 代码&#xff1a; 题目&#xff1a; 示例&#xff1a; 分析&#xff1a; 题目给我们一个二维数组来表示一个披萨&#xff0c;其中‘A’表示披萨上的苹果。 让我们切k-1刀&#xff0c;把披萨切成 k 份&#xff0…

Eslint error, configuration for rule “import/no-cycle“ is invalid

可以参考stackoverflow.comEslint error, configuration for rule "import/no-cycle" is invalid他的意思是有个∞符号不支持&#xff0c;解决方案&#xff0c;把 eslint-plugin-import 的版本增加到 ^2.22.1&#xff0c;重新下载依赖包如&#xff1a;

【分布式共识】Raft算法 选举一个领导者

Raft算法是通过一切以领导者为准的方式&#xff0c;实现一系列值的共识和各节点日志的一致 在分布式系统中&#xff0c;节点可能出现宕机、网络故障等&#xff0c;所以在3个节点的分布式系统中&#xff0c;如何选举出一个Leader节点。比如我们部署一个ZK集群。 成员 Leader领…

《Linux运维总结:Centos7.6之OpenSSH7.4p1升级版本至9.4p1》

Centos通过yum升级OpenSSH 在官方支持更新的CentOS版本&#xff0c;如果出现漏洞&#xff0c;都会通过更新版本来修复漏洞。这时候直接使用yum update就可以升级版本。 yum -y update openssh 但是&#xff0c;CentOS更新需要有一段时间&#xff0c;不能在漏洞刚出来的时候就有…

SpringBoot实现热部署/加载

在我们修改完项目代码后希望不用重启服务器就能把项目代码部署到服务器中(也就是说修改完项目代码后不用重启服务器修改后的项目代码就能生效)。 一、实现devtools原理 <dependency><groupId>org.springframework.boot</groupId><artifactId>spring-…

内网渗透神器CobaltStrike之凭据的导出与存储(八)

简介 Cobalt Strike 是一个流行的渗透测试工具&#xff0c;主要用于模拟高级持续性威胁&#xff08;APT&#xff09;的攻击。它提供了许多功能来操作、持久化和操纵受害者机器。其中&#xff0c;凭据的导出和存储是渗透测试中的一个重要步骤。 凭据导出: Cobalt Strike 通过其…

Vue前端封装一个任务条的组件进行使用

任务条 样式 代码 父组件 <articleSteps :tabs"tabs" :tabs-active-name"tabsActiveName" /><div class"drawer__footer"><el-button v-if"tabsActiveName 1 || tabsActiveName 2" click"backClick">…

面试了几十家,整理出这份车载测试面试题

年前有朋友找工作&#xff0c;跟我说简历改了车载后&#xff0c;收到的打招呼翻了几倍&#xff0c;如今车载测试前景非常广阔&#xff0c;因为越来越多的汽车厂商正在开发新的可智能化的汽车&#xff0c;他们需要测试这些汽车的性能&#xff0c;安全性以及可靠性。 车载测试技…

面试时,如何向HR解释自己频繁跳槽?

有数据显示&#xff0c;现在的职场人&#xff0c;跳槽越来越频繁&#xff0c;95后平均7个月就离职。 对于面试官来说&#xff0c;一个跳槽过于频繁的人总是存在潜在风险&#xff0c;比如抗压力差、稳定性不好、心不定这山望着那山高、职业规划不清晰等等。 我一直强调一个观点…

WXSS的全局样式和局部样式

什么是WXSS? WXSS中的样式和css差不多&#xff0c;是用于给WXML页面来设置样式的&#xff0c;但是WXSS中扩展了rpx尺寸单位和import样式导入 rpx:根据不同的屏幕自动进行适配&#xff0c;把设备屏幕的宽度等分为750份(设备的总宽度750rpx) import: 用于进行样式的导入 通过inp…

get Error: aborted,net::ERR_INCOMPLETE_CHUNKED_ENCODING 200 (OK) 问题解决

先说结论&#xff1a;由于磁盘空间满导致部分数据接口无法正常反馈结果。 今天前线业务人员忽然说&#xff0c;有个页面刷新后白屏。经过验证确实有这种情况。页面上用开发者工具查看请求和报错信息&#xff0c;发现一个请求所有字典数据的接口没有反馈数据&#xff0c…

无人机精细化巡检方案制定:提高效率与准确性的关键

在当前技术日新月异的时代&#xff0c;无人机在多个领域的应用已成为行业标配。但如何制定出一套有效、细致的无人机巡检方案&#xff0c;确保其最大效能&#xff0c;成为许多组织与公司的核心议题。其中&#xff0c;复亚智能在此领域已展现出了卓越的实力与深入的见解。 1. 精…

【LeetCode-困难题】42. 接雨水

题目 题解一&#xff1a;暴力双重for循环&#xff08;以行计算水量&#xff09; 1.先找出最高的柱子有多高&#xff08;max 3&#xff09; 2.然后第一个for为行数&#xff08;1&#xff0c;2&#xff0c;3&#xff09; 3.第二个for计算每一行的雨水量&#xff08;关键在于去除…

06 mysql all查询 和 主键查询 和 非索引列查询

前言 本文主要调试一下 mysql 的如下两种查询语句 我们也来深入的看一下, 究竟如下两个普通的查询, mysql 做了什么事情 1. select * from user where id 991; 2. select * from user; 3. select * from user where name jerry991; 环境介绍 测试表 user schema 如下…

极限学习机(ELM)的原理和matlab代码实现

单隐含层前馈神经网络(Single - hidden Layer Feedforward Neural Network,SLFN)以其良好的学习能力在许多领域得到了广泛的应用。然而,传统的学习算法(如 BP算法等)固有的一些缺点,成为制约其发展的主要瓶颈。前馈神经网络大多采用梯度下降方法,该方法主要存在以下几个方面的缺…

机器人操作系统【02】:如何在 ROS2 中对点云数据进行建模

一、说明 RViz和Gazebo中RADU的模拟进展顺利。在上一篇文章中&#xff0c;我们学习了如何启动机器人并使用远程节点进行操作。在本文中&#xff0c;我们将添加两个视觉传感器。首先&#xff0c;一个图像摄像机&#xff0c;用于在机器人四处移动时查看机器人的实时馈送。其次&am…

Nginx高可用集群

目录 一.简介二.案例1.实现思路2.配置文件修改3.实现效果故障转移机制 一.简介 以提高应用系统的可靠性&#xff0c;尽可能地减少中断时间为目标&#xff0c;确保服务的连续性&#xff0c;达到高可用的容错效果。例如“故障切换”、“双机热备”、“多机热备”等都属于高可用集…

03:TIM定时器

目录 一:TIM 1:介绍 2:定时器的分类 3:基本定时器 4:通用定时器 5:高级定时器 6:定时器的基本结构 二:定时中断功能 A:定时器定时器中断 1:连接图 ​编辑 2:步骤 3:函数介绍 4:代码 三:外部时钟功能 A:定时器外部时钟 1:连接图 2:函数介绍 3:外部时钟代码 一…

【FreeRTOS】【应用篇】任务创建

前言 从本篇开始&#xff0c;将不再太过于关心 FreeRTOS 的内核细节&#xff0c;把重心转移到对 FreeRTOS 的应用上来。 本篇代码大部分参考野火的 FreeRTOS 教程。 一、静态任务和动态任务创建的区别 1. 概念解析 在 FreeRTOS 中&#xff0c;我们可以选择两个不同的函数进…

微信小程序,封装身高体重选择器组件

wxml代码&#xff1a; // 微信小程序的插值语法不支持直接使用Math <wxs src"./ruler.wxs" module"math"></wxs> <view class"ruler-container"><scroll-view scroll-left"{{scrollLeft}}" enhanced"{{tru…