38 线程互斥

news2025/1/12 12:10:45

目录

1.问题
2.互斥相关概念
3.互斥量
4.互斥量接口
5.修改买票代码
6.互斥量原理
7.锁的封装
8.可重入和线程安全

1. 问题

用一个模拟抢票过程的程序引出本节话题,如果有1000张票,设为全局变量,生成3个线程每隔一秒抢一张票,如果票数大于0就继续,小于0就退出,看看结果:

#include <stdio.h>
#include <unistd.h>
#include <pthread.h>
#include <cstring>
#include <thread>
#include <vector>
#include <errno.h>
#include <cstring>

using namespace std;
int ticket = 1000; //票数量
void *buyticket(void *num)
{
    int n = (long)num;
    while (true)
    {
        if (ticket > 0)
        {
            usleep(1000);
            ticket--;
            printf("%d 线程买到票,剩余 %d 张\n", n, ticket);
        }
        else
        {
            break;
        }
    }
}

int main()
{
    vector<pthread_t> v;
    for (int i = 0; i < 3; i++)
    {
        pthread_t tid;
        pthread_create(&tid, nullptr, buyticket, (void*)(long)i);
        v.push_back(tid);
    }

    for(auto ch : v)
    {
        pthread_join(ch, nullptr);
    }

    return 0;
}

在这里插入图片描述

票没有了,剩余负数张,这是有问题的

原因

因为ticket是全局变量,属于共享资源,每个线程都可以访问。当if判断和票数减减时有可能一个线程刚判断票数还有,准备往下执行或–时,其他的线程也来判断,这时,当票数只剩1张时,三个线程都会判断为有票,执行到下面–3次,票数就成了负数

这就是多线程访问共享数据引起的数据不一致问题,线程会在内核返回用户的时候检查切换

–是否安全

对一个全局变量进行多线程并发的–或者++是不是安全的?
cpu在对一个数据减减时需要三步操作,所以是不安全的
在这里插入图片描述

1.先将ticket读入到寄存器中
2.cpu内部计算
3.将计算结果写回内存
上面每一步都对应一条汇编操作,只有一条汇编指令才是原子的,上面的三步操作中都有可能切换线程。线程1先读入数据。if判断属于逻辑运算,需要读入数据,进行判断,跳转执行,也不是原子的。如果票数只剩1张,几条线程都判断大于1进入内部,当1条线程–后将数据改为0写回内存,其他线程进来执行,别忘了,–操作需要重新从内存中读入数据,所以会在0的基础上继续–,就变成了-1

怎么解决
对共享收据的任何访问,保证在任何时候只有一个执行流访问,就是互斥

2. 互斥相关概念

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

3. 互斥量

大部分情况,线程使用的数据都是局部变量,变量的地址空间在线程栈空间内,这种情况,变量归属单个线程,其他线程无法获得这种变量
但有时候,很多变量都需要在线程间共享,这样的变量称为共享变量,可以通过数据的共享,完成线程之间的交互
多个线程并发的操作共享变量,会带来一些问题
上面的抢票由下面三个地方可能被切换:
1.if语句判断为真以后,代码可以并发的切换到其他线程
2.usleep这个模拟漫长业务的过程,可能会有多个线程进入该代码片段
3.–ticket操作本身不是原子的

下面是–的部分汇编代码

取出ticket–部分的汇编代码
objdump -d a.out > test.objdump
152 40064b: 8b 05 e3 04 20 00 mov 0x2004e3(%rip),%eax # 600b34 <ticket>
153 400651: 83 e8 01 sub $0x1,%eax
154 400654: 89 05 da 04 20 00 mov %eax,0x2004da(%rip) # 600b34 <ticket>

操作不是原子的,对应了三条汇编指令:
load:将共享变量ticket从内存加载到寄存器中
update:更新寄存器里面的值,执行-1操作
store:将新值,从寄存器写回共享变量ticket的内存地址

要解决上面的问题,需要做到三点:
1.代码必须要有互斥行为:当代码进入临界区执行时,不允许其他线程进入临界区
2.如果多个线程同时要执行临界区的代码,并且临界区没有线程在执行,那么只能允许一个线程进入该临界区
3.如果线程不在临界区执行,那么该线程不能阻止其他线程进入临界区

要做到这三点,需要一把锁,linux上提供的这把锁叫互斥量
在这里插入图片描述

4. 互斥量接口

初始化互斥量

初始化互斥量由两种方法:
方法1.静态分配
这种方法申请的锁会自动解锁

pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER

方法2.动态分配

int pthread_mutex_init(pthread_mutex_t *restrict mutex, const pthread_mutexattr_t
*restrict attr);
参数:
mutex:要初始化的互斥量
attr:NULL

销毁互斥量

需要注意:
使用PTHREAD_MUTEX_INTIALIZER初始化的互斥量不需要销毁
不要销毁一个已经加锁的互斥量
已经销毁的互斥量,要确保后面不会有线程再尝试加锁

int pthread_mutex_destroy(pthread_mutex_t *mutex);

互斥量加锁和解锁

int pthread_mutex_lock(pthread_mutex_t *mutex);
int pthread_mutex_unlock(pthread_mutex_t *mutex);
返回值:成功返回0,失败返回错误号

调用pthread_lock时,可能会遇到以下情况:

  • 互斥量处于未锁状态,该函数会将互斥量锁定,并返回成功
  • 发起函数调用时,其他线程已经锁定互斥量,或者存在其他线程同时申请互斥量,但没有竞争到互斥量,那么pthread_lock调用会陷入阻塞(执行流被挂起),等待互斥量解锁

加锁总结

加锁的本质:用时间换取安全
加锁的表现:线程对于临界区代码串行执行
加锁原则:尽量的保证临界区代码越少越好,串行执行的代码就越少,其他线程等的时间就越少

5. 修改买票代码

局部变量锁

#include <stdio.h>
#include <unistd.h>
#include <pthread.h>
#include <cstring>
#include <thread>
#include <vector>
#include <errno.h>
#include <cstring>

using namespace std;
int ticket = 1000; //票数量

struct threadData
{
    threadData(int num, pthread_mutex_t *_lock)
    {
        i = num;
        lock = _lock;
    }

    int i;
    pthread_mutex_t *lock;
};
void *buyticket(void *num)
{
    threadData* td = static_cast<threadData*>(num);
    while (true)
    {
        //加锁
        pthread_mutex_lock(td->lock);
        if (ticket > 0)
        {
            usleep(1000);
            ticket--;
            printf("%d 线程买到票,剩余 %d 张\n", td->i, ticket);
            //解锁
            pthread_mutex_unlock(td->lock);
        }
        else
        {
            pthread_mutex_unlock(td->lock);
            break;
        }

        usleep(13);  //买完票不会去买下一张,延时模拟过程
    }
}

int main()
{
    vector<pthread_t> v;
    vector<threadData*> td;
    // 初始化互斥量
    pthread_mutex_t lock;
    pthread_mutex_init(&lock, nullptr);
    
    for (int i = 0; i < 3; i++)
    {
        td.push_back(new threadData(i, &lock));
        pthread_t tid;
        pthread_create(&tid, nullptr, buyticket, td[i]);
        v.push_back(tid);
    }

    for(auto ch : v)
    {
        pthread_join(ch, nullptr);
    }

    return 0;
}

在这里插入图片描述

这次运行正常了,买票之后加上usleep,省略买成功后的动作,如果没有,会造成加锁的线程刚买完又立马继续加锁,导致其他线程难以得到锁的使用权,无法进入判断。线程对锁的竞争能力是不同的

纯互斥环境,如果锁分配不合理,容易导致其他线程的饥饿问题。不是说只要有互斥,必有饥饿。适合纯互斥的场景,就用互斥
让所有的线程获取锁,按照一定顺序获取资源,就是同步

线程申请锁成功,才能向后执行,不成功则阻塞等待。在临界区,线程可以切换,但线程切换出去是持有锁被切走,不在期间,照样没有人能进入临界区访问资源。对于其他线程来讲,一个线程要么没有锁,要么释放锁,当前线程访问临界区的过程,对于其他线程都是原子的。不释放锁,其他线程一直申请不到锁,就阻塞着

全局变量锁

#include <stdio.h>
#include <unistd.h>
#include <pthread.h>
#include <cstring>
#include <thread>
#include <vector>
#include <errno.h>
#include <cstring>

using namespace std;
int ticket = 1000; //票数量
//全局变量锁,自动解锁
pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;

struct threadData
{
    threadData(int num/*, pthread_mutex_t *_lock*/)
    {
        i = num;
        //lock = _lock;
    }

    int i;
    //pthread_mutex_t *lock;
};
void *buyticket(void *num)
{
    threadData* td = static_cast<threadData*>(num);
    while (true)
    {
        //加锁
        pthread_mutex_lock(&lock);
        if (ticket > 0)
        {
            usleep(1000);
            ticket--;
            printf("%d 线程买到票,剩余 %d 张\n", td->i, ticket);
            //解锁
            pthread_mutex_unlock(&lock);
        }
        else
        {
            pthread_mutex_unlock(&lock);
            break;
        }

        usleep(13);  //买完票不会去买下一张,延时模拟过程
    }
}

int main()
{
    vector<pthread_t> v;
    vector<threadData*> td;
    // 初始化互斥量
    pthread_mutex_t lock;
    //pthread_mutex_init(&lock, nullptr);
    
    for (int i = 0; i < 3; i++)
    {
        td.push_back(new threadData(i/*, &lock*/));
        pthread_t tid;
        pthread_create(&tid, nullptr, buyticket, td[i]);
        v.push_back(tid);
    }

    for(auto ch : v)
    {
        pthread_join(ch, nullptr);
    }

    for(auto ch : td)
    {
        delete ch;
    }

    //pthread_mutex_destroy(&lock);
    return 0;
}

全局变量不需要初始化和销毁

6. 互斥量原理

为了实现互斥锁,大多数体系结构都提供了swap或exchange指令,该指令的作用是把寄存器和内存单元的数据相交换,由于只有一条指令,保证了原子性,即使是多处理器平台,访问内存的,总线周期也有先后,一个处理器上的交换指令执行时另一个处理器的交换指令只能等待总线周期,现在把lock和unlock的伪代码改一下

在这里插入图片描述
在这里插入图片描述
将互斥量简单看做上述样子。初始化互斥量就会在内存里申请一个整数1,每一个线程上锁的过程就是上面的代码,al是eax寄存器的低16位,将这个寄存器值设为0,线程进来后寄存器和互斥量1进行交换,寄存器中变为1,把共享的锁以汇编的方式交换到了自己的上下文。内存中变为0,判断寄存器里值大于0就返回上锁成功。这时其他线程进来执行时,寄存器和内存中都为0,判断后就不满足条件而挂起等待。所谓的“锁”只有一个,只能被一个线程拥有,保证了加锁过程的原子性

在这里插入图片描述

解锁的过程将内存中互斥值改为1,然后唤醒等待的线程就可以像上面一样继续有一个线程得到锁。这里为什么不也用交换的方式,而是直接赋值?这样可以让一个线程上锁,另一个线程可以解锁,对解锁的一方没有要求。如果上锁的线程卡死了,不解锁的话其他线程也无法执行,所以可以解决这种情况

7. 锁的封装

一个锁类,有加锁和去锁的功能,再用一个类封装这个锁类成员,构造自动上锁,析构去锁。修改买票功能,线程判断票数的时候上锁,–完后去锁,避免延迟模式的功能到临界区,可以加一个域括号

锁类

#pragma once
#include <pthread.h>

class Mutex
{
public:
    Mutex(pthread_mutex_t *_lock)
    {
        m_lock = _lock;
    }

    void Lock()
    {
        pthread_mutex_lock(m_lock);
    }

    void unLock()
    {
        pthread_mutex_unlock(m_lock);
    }

private:
    pthread_mutex_t *m_lock;
};

class LockGuard
{
public:
    LockGuard(pthread_mutex_t *_lock)
    :m(_lock)
    {
        m.Lock();
    }

    ~LockGuard()
    {
        m.unLock();
    }

private:
    Mutex m;
};

买票

#include <stdio.h>
#include <unistd.h>
#include <cstring>
#include <thread>
#include <vector>
#include <errno.h>
#include <cstring>
#include "LockGuard.hpp"

using namespace std;
int ticket = 1000; //票数量
//全局变量锁,自动解锁
pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;

struct threadData
{
    threadData(int num/*, pthread_mutex_t *_lock*/)
    {
        i = num;
        //lock = _lock;
    }

    int i;
    //pthread_mutex_t *lock;
};
void *buyticket(void *num)
{
    threadData* td = static_cast<threadData*>(num);
    while (true)
    {
        //加锁
        //pthread_mutex_lock(&lock);
        {
            LockGuard mutex(&lock);
            if (ticket > 0)
            {
                usleep(1000);
                ticket--;
                printf("%d 线程买到票,剩余 %d 张\n", td->i, ticket);
                // 解锁
                // pthread_mutex_unlock(&lock);
            }
            else
            {
                // pthread_mutex_unlock(&lock);
                break;
            }
        }
        usleep(13);  //买完票不会去买下一张,延时模拟过程
    }
}

int main()
{
    vector<pthread_t> v;
    vector<threadData*> td;
    // 初始化互斥量
    //pthread_mutex_t lock;
    //pthread_mutex_init(&lock, nullptr);
    
    for (int i = 0; i < 3; i++)
    {
        td.push_back(new threadData(i/*, &lock*/));
        pthread_t tid;
        pthread_create(&tid, nullptr, buyticket, td[i]);
        v.push_back(tid);
    }

    for(auto ch : v)
    {
        pthread_join(ch, nullptr);
    }

    for(auto ch : td)
    {
        delete ch;
    }

    //pthread_mutex_destroy(&lock);
    return 0;
}

这种类自动加锁解锁方式称为RAII方式

8. 可重入和线程安全

概念

线程安全:多个线程并发同一段代码时,不会出现不同的结果。常见对全局变量或者静态变量进行操作,并且没有锁保护的情况会出现该问题
重入:同一个函数被不同的执行流调用,当前一个线程还没有执行完,就有其他的执行流再次进入,我们称之为重入。一个函数在重入的情况相爱,运行结果不会出现任何不同或者任何问题,则该函数被称为可重入函数,否则,是不可重入函数

常见的线程不安全的情况

  • 不保护共享变量的函数
  • 函数状态随着被调用,状态发生变化的函数
  • 返回指向静态变量指针的函数
  • 调用线程不安全函数的函数

常见的线程安全情况

每个线程对全局变量或者静态变量只有读取的权限,没有写入的权限,一般来说这些线程是安全的
类或者接口对于线程来说都是原子操作
多个线程之间的切换不会导致该接口的执行结果存在二义性

常见的不可重入

调用了malloc/free函数,因为malloc函数是用全局链表来管理堆的
调用了标准I/O库函数,很多实现都以不可重入的方式使用全局数据结构
可重入函数体内使用了静态的数据结构

常见可重入

不适用全局变量或静态变量
不适用malloc或者new开辟空间
不调用不可重入函数
不返回静态或全局数据,所有数据都由函数的调用者提供
使用本地数据,或者通过制作全局数据的本地拷贝来保护全局数据

可重入和线程安全联系

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

可重入和线程安全的区别

可重入函数是线程安全函数的一种
线程安全不一定是可重入的,而可重入函数一定是线程安全的
如果将对临界资源的访问加上锁,这个函数是线程安全的,但如果这个重入函数锁还未释放则会产生死锁,是不可重入的

线程安全描述的是并发的情况,重入描述的是函数的特性。线程安全不一定是可重入的,可重入一定是线程安全的

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

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

相关文章

奥比中光Astra RGBD ROS1配置(乐视RGBD)

早年买了一款乐视RGBD一直落灰&#xff0c;最近做一个机器人项目想重新使用起来。发现官方给的RGBD包和github上面的ros_astra_camera包并不能很好的驱动这款相机。研究了一下进行补足。 最终的结果是彩色&#xff0c;红外以及深度退昂均能实时读取。具体过程如下&#xff1a; …

【LeetCode】---剑指Offer 31.栈的弹出压入序列

一、题目描述&#xff1a; 二、算法原理&#xff1a; 核心思想&#xff1a; 入栈的栈顶元素跟出栈序列进行匹配&#xff0c;入一个匹配一个。注意&#xff1a;结束的标志就是入栈序列走完了。 三、代码实现&#xff1a; class Solution { public:/*** 代码中的类名、方法名、…

每天五分钟深度学习:如何理解梯度下降算法可以逼近全局最小值?

本文重点 上节课程中,我们已经知道了逻辑回归的代价函数J。要想最小化代价函数,我们需要使用梯度下降算法。 梯度下降算法地直观理解: 为了可视化,我们假设w和b都是单一实数,实际上,w可以是更高地维度。 代价函数J是在水平轴w和b上的曲面,因此曲面的高度就是J(w,b)在…

低功耗数字IC后端设计实现典型案例| UPF Flow如何避免工具乱用Always On Buffer?

下图所示为咱们社区低功耗四核A7 Top Hierarchical Flow后端训练营中的一个案例&#xff0c;设计中存在若干个Power Domain&#xff0c;其中Power Domain2(简称PD2)为default Top Domain&#xff0c;Power Domain1&#xff08;简称PD1&#xff09;为一个需要power off的domain&…

21 如何进行高保真压测和服务扩容?

在后台架构中&#xff0c;压测非常常见&#xff0c;也是必须的工作。它能够帮我们发现微服务架构中的性能瓶颈&#xff0c;以及知道构建的微服务能承载的流量极限值。 但实际情况是&#xff0c;很多压测并不能发现瓶颈点和微服务所能承载的真实流量极限值。一方面是因为压测时…

【专篇】DDR4 SDRAM-01总体介绍

概念 DDR4 SDRAM(Double-Data-Rate Fourth Generation Synchronous Dynamic Random Access Memory)是一种先进的高性能存储器规格,是DDR3 SDRAM的后续产品。自2011年首次由三星电子制造并公布以来,它已经成为现代计算机系统中广泛采用的内存标准。 主要特点: 更高带宽: DD…

Spring6 当中的 Bean 循环依赖的详细处理方案+源码解析

1. Spring6 当中的 Bean 循环依赖的详细处理方案源码解析 文章目录 1. Spring6 当中的 Bean 循环依赖的详细处理方案源码解析每博一文案1.1 Bean的循环依赖1.2 singletion 下的 set 注入下的 Bean 的循环依赖1.3 prototype下的 set 注入下的 Bean 的循环依赖1.4 singleton下的构…

什么是人才测评系统?

人才测评系统是一个整体的框架&#xff0c;里面有很多个部分构成&#xff0c;既有常见的人才测评方法&#xff0c;也包含有招聘的目的。随着科学研究的不断深入&#xff0c;人才测评系统已经变得更加完善&#xff0c;将现代心理学和管理学知识吸纳到人才测评理论中来&#xff0…

OpenAI神秘搞事,GPT-4.5默默上线?推理碾压GPT-4网友震惊,奥特曼笑而不语

【新智元导读】就在昨夜&#xff0c;整个AI社区都被一个神秘大模型震撼到了&#xff1a;它名为gpt2-chatbot&#xff0c;性能直接超越很多开源模型和GPT-4&#xff01;网友们展开猜测&#xff0c;有说它是GPT-4.5的&#xff0c;有说是GPT-5的&#xff0c;还有人猜它是GPT-4Q*&a…

深度学习论文: MobileNetV4 - Universal Models for the Mobile Ecosystem及其PyTorch实现

深度学习论文: MobileNetV4 - Universal Models for the Mobile Ecosystem及其PyTorch实现 MobileNetV4 - Universal Models for the Mobile Ecosystem PDF: https://arxiv.org/pdf/2404.10518.pdf PyTorch代码: https://github.com/shanglianlm0525/CvPytorch PyTorch代码: ht…

ARP学习及断网攻击

1.什么是ARP ARP&#xff08;Address Resolution Protocol&#xff09;是一种用于在IPv4网络中将IP地址映射到MAC地址的协议。在计算机网络中&#xff0c;每个网络接口都有一个唯一的MAC地址&#xff08;Media Access Control address&#xff09;&#xff0c;用于识别网络设备…

JAVA同城服务美容美发到店服务上门服务系统源码微信小程序+微信公众号+H5+APP

随着科技的飞速发展&#xff0c;互联网和移动互联网已经渗透到我们生活的方方面面&#xff0c;同城服务美容美发到店服务上门服务系统应运而生&#xff0c;为整个行业带来了巨大的变革和无限的可能。该系统的重要性和优势不言而喻&#xff0c;对于行业发展和用户需求的影响深远…

LLaMA3(Meta)微调SFT实战Meta-Llama-3-8B-Instruct

LlaMA3-SFT LlaMA3-SFT, Meta-Llama-3-8B/Meta-Llama-3-8B-Instruct微调(transformers)/LORA(peft)/推理 项目地址 https://github.com/yongzhuo/LLaMA3-SFT默认数据类型为bfloat6 备注 1. 非常重要: weights要用bfloat16/fp32/tf32(第二版大模型基本共识), 不要用fp16, f…

Win环境下Python语言通过ODBC/JDBC连接SinoDB数据库

1. 前言 本文介绍在Windows环境下Python语言通过ODBC和JDBC的方式连接SinoDB数据库&#xff0c;并对数据库进行基本操作。 2. 软件安装包 SinoDB数据库&#xff1a;SinoDB 12.10.FC8   SinoDB CSDK(Windows)&#xff1a;SinoDB Client-SDK 4.10.FC8   Python语言开发包&am…

Spring-aop切面环绕通知

1.pom引入 <!-- 切面依赖--> <dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-aop</artifactId> </dependency> 2.定义注解&#xff1a;AroundAnnotation import java.lang.annotation…

公文写作笔记

标题 最后一行的日期&#xff0c;后边占4个格子。两个数字占一格。落款单位在日期的正上方。 格式积累 内容&#xff1a; ①开头&#xff1a;缘由 ②主题&#xff1a;对策&#xff08;别人做得好&#xff0c;就借鉴&#xff09; ③结尾&#xff1a;简单的总结&#xff08;字…

Linux基础——Linux开发工具(上)_vim

前言&#xff1a;在了解完Linux基本指令和Linux权限后&#xff0c;我们有了足够了能力来学习后面的内容&#xff0c;但是在真正进入Linux之前&#xff0c;我们还得要学会使用Linux中的几个开发工具。而我们主要介绍的是以下几个&#xff1a; yum, vim, gcc / g, gdb, make / ma…

OpenHarmony 实战开发——自测试执行框架

OpenHarmony为开发者提供了一套全面的开发自测试框架OHA-developer_test&#xff0c;开发者可根据测试需求开发相关测试用例&#xff0c;开发阶段提前发现缺陷&#xff0c;大幅提高代码质量。 本文从基础环境构建&#xff0c;用例开发&#xff0c;编译以及执行等方面介绍OpenH…

FlaUI

FlaUI是一个基于微软UIAutomation技术&#xff08;简称UIA&#xff09;的.NET库&#xff0c;它主要用于对Windows应用程序&#xff08;如Win32、WinForms、WPF、Store Apps等&#xff09;进行自动化UI测试。FlaUI的前身是TestStack.White&#xff0c;由Roemer开发&#xff0c;旨…

23 重构:烟囱式、平台化、中台化的架构

上一讲里&#xff0c;我们介绍了两大类型的系统升级重构方案&#xff0c;还介绍了如何进行重构版本的上线&#xff0c;以及如何平滑地完成新老版本切换的方案。在本讲里&#xff0c;将会具体介绍如何判断系统发展到什么阶段需要重构&#xff0c;以及如何实施重构。 系统稳定性…