Linux 线程互斥

news2024/9/23 6:03:04

文章目录

  • 3. Linux 线程互斥
    • 3.1 相关概念
    • 3.2 互斥量mutex
    • 3.3 互斥量的接口
    • 3.4 改进3.2中的代码
    • 3.5 互斥量(锁)的原理
    • 3.6 封装一下原生锁的接口,RAII风格的锁
    • 3.7 可重入 和 线程安全

3. Linux 线程互斥

3.1 相关概念

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

3.2 互斥量mutex

  • 大部分情况,线程使用的数据都是局部变量,变量的地址空间在线程栈空间内,这种情况,变量归属单个线程,其他线程无法获得这种变量。
  • 但有时候,很多变量都需要在线程间共享,这样的变量称为共享变量,可以通过数据的共享,完成线程之间的交互。
  • 多个线程并发的操作共享变量,会带来一些问题。 看下面的代码
struct ThreadData
{   
    ThreadData(int i) 
    {
        _threadName = "Thread-" + to_string(i);
    }
    string _threadName;
};

// 共享资源,票
int ticket = 1000;

void* GetTicket(void* args)
{
    ThreadData* data = static_cast<ThreadData*>(args);
    const char* name = data->_threadName.c_str();
    while(true) {
        // 抢票
        printf("%s get a ticket, ticket: %d\n", name, ticket);
        ticket--;
        if(ticket <= 0) {
            // 没票了,该线程就break
            printf("%s can not get ticket, done\n", name);
            break;
        }
        usleep(1000);
    }
    return nullptr;
}

int main()
{
    vector<pthread_t> tids;
    vector<ThreadData*> datas;
    for(size_t i = 1; i <= NUM; ++i) {
        ThreadData* data = new ThreadData(i);
        datas.push_back(data);
        pthread_t tid;
        pthread_create(&tid, nullptr, GetTicket, datas[i-1]);
        tids.push_back(tid);
    }

    // 善后处理
    for(const auto& t : tids)    pthread_join(t, nullptr);
    for(const auto& d : datas)   delete d;
    return 0;
}

运行后可以看到,票出现了负数,所以出现了问题

image-20240919171807936

该现象叫共享资源被多线程访问后出现数据不一致问题,为什么会出现呢?

  • if 语句判断条件为真以后,代码可以并发的切换到其他线程
  • usleep这个模拟漫长业务的过程,在这个漫长的业务过程中,可能有很多个线程会进入该代码段
  • ticket-- 操作本身就不是一个原子操作

-- 操作并不是原子操作,而是对应三条汇编指令:

  • load :将共享变量ticket从内存加载到寄存器中。线程在执行的时候,将共享数据加载到CPU寄存器的本质是:把数据的内容,变成自己的上下文,即以拷贝的方式,给自己单独拿了一份。
  • update : 更新寄存器里面的值,执行-1操作
  • store :将新值,从寄存器写回共享变量ticket的内存地址

要解决以上问题,需要做到三点:

  • 代码必须要有互斥行为:当代码进入临界区执行时,不允许其他线程进入该临界区。
  • 如果多个线程同时要求执行临界区的代码,并且临界区没有线程在执行,那么只能允许一个线程进入该临界区。
  • 如果线程不在临界区中执行,那么该线程不能阻止其他线程进入临界区。

要做到这三点,本质上就是需要一把锁。Linux上提供的这把锁叫互斥量。

image-20240922150555936

3.3 互斥量的接口

初始化互斥量

方法1,静态分配:
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
方法2,动态分配:
int pthread_mutex_init(pthread_mutex_t *restrict mutex, const pthread_mutexattr_t *restrictattr);
参数:
mutex:要初始化的互斥量
attr:不关心,暂时设置为NULL

销毁互斥量

需要注意:

  • 使用 PTHREAD_ MUTEX_ INITIALIZER 初始化的互斥量不需要销毁
  • 不要销毁一个已经加锁的互斥量
  • 已经销毁的互斥量,要确保后面不会有线程再尝试加锁
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调用会陷入阻塞(执行流被挂起),等待互斥量解锁。

注意:

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

3.4 改进3.2中的代码

// 方法1,静态分配
struct ThreadData
{   
    ThreadData(int i) 
    {
        _threadName = "Thread-" + to_string(i);
    }
    string _threadName;
};

// 共享资源,票
int ticket = 1000;

void* GetTicket(void* args)
{
    static pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;    // 也可以使用全局变量
    ThreadData* data = static_cast<ThreadData*>(args);
    const char* name = data->_threadName.c_str();
    while(true) {
        pthread_mutex_lock(&lock);    // 申请锁成功,才能往后执行,不成功,线程就阻塞等待
        if(ticket > 0) {
            usleep(1000);
            // 抢票
            printf("%s get a ticket, ticket: %d\n", name, ticket);
            ticket--;
            pthread_mutex_unlock(&lock);
        }
        else {
            // 没票了,该线程就break
            printf("%s can not get ticket, done\n", name);
            pthread_mutex_unlock(&lock);
            break;
        }
        /* 防止这个线程抢完票后又立即去申请锁,让它sleep()一会儿,给其它正在被阻塞的线程一个机会,否则该线程迟迟分配不到资源, 会导致饥饿问题 */
        usleep(15);     
    }
    return nullptr;
}

int main()
{
    vector<pthread_t> tids;
    vector<ThreadData*> datas;
    for(size_t i = 1; i <= NUM; ++i) {
        ThreadData* data = new ThreadData(i);
        datas.push_back(data);
        pthread_t tid;
        pthread_create(&tid, nullptr, GetTicket, datas[i-1]);
        tids.push_back(tid);
    }

    // 善后处理
    for(const auto& t : tids)    pthread_join(t, nullptr);
    for(const auto& d : datas)   delete d;
    return 0;
}
// 方法2,动态分配:
struct ThreadData
{   
    ThreadData(int i, pthread_mutex_t* lock) 
    {
        _threadName = "Thread-" + to_string(i);
        _lock = lock;
    }
    string _threadName;
    pthread_mutex_t* _lock;
};

// 共享资源,票
int ticket = 1000;

void* GetTicket(void* args)
{
    ThreadData* data = static_cast<ThreadData*>(args);
    const char* name = data->_threadName.c_str();
    while(true) {
        pthread_mutex_lock(data->_lock);    // 申请锁成功,才能往后执行,不成功,线程就阻塞等待
        if(ticket > 0) {
            usleep(1000);
            // 抢票
            printf("%s get a ticket, ticket: %d\n", name, ticket);
            ticket--;
            pthread_mutex_unlock(data->_lock);
        }
        else {
            // 没票了,该线程就break
            printf("%s can not get ticket, done\n", name);
            pthread_mutex_unlock(data->_lock);
            break;
        }
        /*防止这个线程抢完票后又立即去申请锁,让它sleep()一会儿,给其它正在被阻塞的线程一个机会,否则该线程迟迟分配不到资源, 会导致饥饿问题*/
        usleep(15);    
    }
    return nullptr;
}

int main()
{
    vector<pthread_t> tids;
    vector<ThreadData*> datas;
    pthread_mutex_t lock;
    pthread_mutex_init(&lock, nullptr);
    for(size_t i = 1; i <= NUM; ++i) {
        ThreadData* data = new ThreadData(i, &lock);
        datas.push_back(data);
        pthread_t tid;
        pthread_create(&tid, nullptr, GetTicket, datas[i-1]);
        tids.push_back(tid);
    }

    // 善后处理
    for(const auto& t : tids)    pthread_join(t, nullptr);
    for(const auto& d : datas)   delete d;
    return 0;
}

image-20240922154223283

看到了我们想要看到的,票数并没有出现负数


需要保证:

  • 外面的线程,需要排队等待
  • 已经执行完临界区的线程,不能立马申请锁,需要排到队列的尾部
  • 让所有的线程获取锁,按照一定的顺序。线程按照一定的顺序性获取资源,就叫做同步问题

每个线程访问临界区之前都需要访问锁,说明锁本身也是共享资源,所以申请锁和释放锁的过程要是原子的


纯互斥环境,如果锁分配不够合理,容易导致其他线程的饥饿问题
注意:不是说只要有互斥,必有饥饿。


在临界区中,该线程可以被切换吗
可以切换,因为在线程被切出去的时候,是持有锁被切走的,线程不在期间,其它被线程不能访问临界资源,会一直被阻塞,直到pthread_mutex_unlock()会唤醒这些线程。这样,就保证了该线程在执行临界区的时候,对其它线程是原子的

3.5 互斥量(锁)的原理

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

image-20240922190946042

lock:

最重要的是xchgb语句
交换的本质:线程把内存中的数据(共享),交换到CPU的寄存器中。即线程把一个共享数据交换到自己的硬件上下文中。而线程的上下文是线程独有的,这样该线程就持有了一个锁。

unlock:

将mutex制1

3.6 封装一下原生锁的接口,RAII风格的锁

// LockGuard.hpp
#pragma once
#include <pthread.h>
class Mutex
{
public:
    Mutex(pthread_mutex_t* lock) : _lock(lock) {}

    void Lock()
    {
        pthread_mutex_lock(_lock);
    }

    void Unlock()
    {
        pthread_mutex_unlock(_lock);
    }
private:
    pthread_mutex_t* _lock;
};

class LockGuard
{
public:
    LockGuard(pthread_mutex_t *lock) : _mutex(lock) 
    {
        _mutex.Lock();
    }

    ~LockGuard() 
    {
        _mutex.Unlock();
    }
private:
    Mutex _mutex;
};

改进一下3.4中方法1的GetTicket()

void* GetTicket(void* args)
{
    static pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;    // 也可以使用全局变量
    ThreadData* data = static_cast<ThreadData*>(args);
    const char* name = data->_threadName.c_str();
    while(true) {
        {
            LockGuard lockGuard(&lock);
            if(ticket > 0) {
                usleep(1000);
                // 抢票
                printf("%s get a ticket, ticket: %d\n", name, ticket);
                ticket--;
                pthread_mutex_unlock(&lock);
            }
            else {
                // 没票了,该线程就break
                printf("%s can not get ticket, done\n", name);
                pthread_mutex_unlock(&lock);
                break;
            }
        }
        /* 防止这个线程抢完票后又立即去申请锁,让它sleep()一会儿,给其它正在被阻塞的线程一个机会,否则该线程迟迟分配不到资源, 会导致饥饿问题 */
        usleep(15);     
    }
    return nullptr;
}

这样,我们的代码中就不需要关心烦人和易忘的加锁和解锁,只要定义一个临时的LockGuard对象,就可以自动完成。

3.7 可重入 和 线程安全

概念

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

可重入与线程安全联系

  • 函数是可重入的,那就是线程安全的
  • 函数是不可重入的,那就不能由多个线程使用,有可能引发线程安全问题

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

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

相关文章

web自动化学习笔记

目录 一、web自动化测试环境搭建二、web自动化测试第一个脚本三、selenium 8种定位方式3.1、通过id定位3.2、通过name定位3.3、通过tag_name&#xff08;标签名&#xff09;定位3.4、通过class值进行定位3.5、通过链接文本定位3.6、通过部分链接文本定位3.7、通过xpath定位3.8、…

MySQL:SQL语句执行过程

本篇文章会分析下一个 SQL 语句在 MySQL 中的执行流程&#xff0c;包括 SQL 的查询在 MySQL 内部会怎么流转&#xff0c;SQL 语句的更新是怎么完成的。 在分析之前我会先带着你看看 MySQL 的基础架构&#xff0c;知道了 MySQL 由那些组件组成以及这些组件的作用是什么&#xf…

Spring_AMQP

文章目录 一、SpringAMQP二、SpringAMQP应用2.1、消息发送2.2、消息接收 一、SpringAMQP SpringAMQP是基于RabbitMQ封装的一套模板&#xff0c;并且还利用SpringBoot对其实现了自动装配&#xff0c;使用起来非常方便。 SpringAmqp的官方地址。 SpringAMQP提供了三个功能&am…

python27 安装pywinauto-0.6.8

文章目录 环境所需依赖依赖查找所需依赖链接1. comtypes2. six3. pyWin324. (optional) Pillow (to make screenshoots)5. pywinauto 安装结果 环境 windows 11 Python 2.7 setuptools 18.0.1 pip 18.1 所需依赖 依赖查找 按照官方文档&#xff08;https://pywinauto.readt…

MMROTATE 1.X特征图可视化(绘制Heat Map)

本文参考MMYOLO官方的特征图可视化教程&#xff0c;对MMROTATE相关算法进行特征图可视化 1. 新建featmap_vis_demo.py文件 在mmrotate项目文件夹下新建 featmap_vis_demo.py &#xff1a; # Copyright (c) OpenMMLab. All rights reserved. import argparse import os from t…

java intellij idea开发步骤,使用指南,工程创建与背景色字体配置,快捷键

intellij idea2021 配置背景色&#xff0c;字体大小&#xff0c;主题 快捷键

STM32系统时钟

时钟为单片机提供了稳定的机器周期&#xff0c;从而使我们的系统能够正常的运行 时钟就像我们人的心脏&#xff0c;一旦有问题就整个都会崩溃 stm32有很多外设&#xff0c;但不是所有的外设都使用同一种时钟频率工作&#xff0c;比如我们的内部看门狗和RTC 只要30几k的频率就…

计算机毕业设计推荐-基于python的白酒销售数据可视化分析

精彩专栏推荐订阅&#xff1a;在下方主页&#x1f447;&#x1f3fb;&#x1f447;&#x1f3fb;&#x1f447;&#x1f3fb;&#x1f447;&#x1f3fb; &#x1f496;&#x1f525;作者主页&#xff1a;计算机毕设木哥&#x1f525; &#x1f496; 文章目录 一、白酒销售数据…

react:React Hook函数

使用规则 只能在组件中或者其他自定义的Hook函数中调用 只能在组件的顶层调用&#xff0c;不能嵌套在if、for、 其他函数中 基础Hook 函数 useState useState是一个hook函数&#xff0c;它允许我们向组件中添加一个状态变量&#xff0c;从而控制影响组件的渲染结果 示例1…

[Excel VBA]如何使用VBA自动生成图表

在Excel中&#xff0c;图表是可视化数据的重要工具。以下是一个VBA代码示例&#xff0c;帮助你自动生成图表。 1. 代码说明 该代码会根据指定数据范围创建一个柱状图&#xff0c;并设置图表的基本属性。 2. VBA代码 Sub CreateChart()Dim ws As WorksheetDim chartObj As Ch…

百度营销转化追踪(网页JS布码)

引言&#xff1a;使用百度营销api配置网站上各个模块组件的转化追踪&#xff0c;统计网站上的各组件模块点击等信息。 一、选择接入方式&#xff08;本文选择的是网页JS布码&#xff09; 参考文档&#xff1a;百度营销-商业开发者中心百度开发者中心是一个面向开发者的知识分享…

Java启动Tomcat: Can‘t load IA 32-bit .dll on a AMD 64-bit platform报错问题解决

&#x1f3ac; 鸽芷咕&#xff1a;个人主页 &#x1f525; 个人专栏: 《C干货基地》《粉丝福利》 ⛺️生活的理想&#xff0c;就是为了理想的生活! 专栏介绍 在软件开发和日常使用中&#xff0c;BUG是不可避免的。本专栏致力于为广大开发者和技术爱好者提供一个关于BUG解决的经…

Java-数据结构-排序-(二) (๑¯∀¯๑)

文本目录&#xff1a; ❄️一、交换排序&#xff1a; ➷ 1、 冒泡排序&#xff1a; ▶ 代码&#xff1a; ➷ 2、 快速排序&#xff1a; ☞ 基本思想&#xff1a; ☞ 方法一&#xff1a;Hoare法 ▶ 代码&#xff1a; ☞ 方法二&#xff1a;挖坑法 ▶ 代码&#xff1a; ☞ 方法三…

GNU编译器(GCC):编译的4个过程及.elf、.list、.map文件功能说明

0 参考资料 GNU-LD-v2.30-中文手册.pdf GNU linker.pdf1 前言 一个完整的编译工具链应该包含以下4个部分&#xff1a; &#xff08;1&#xff09;编译器 &#xff08;2&#xff09;汇编器 &#xff08;3&#xff09;链接器 &#xff08;4&#xff09;lib库 在GNU工具链中&…

Linux-文件的压缩、解压

Linux系统常见有两种压缩格式&#xff0c;后缀分别是&#xff1a; .tar 称之为tarball&#xff0c;简单的将文件组装到一个.tar的文件内&#xff0c;并没有太多的文件体积减少&#xff0c;仅仅是简单的封装.gz gzip格式压缩文件&#xff0c;可以极大的减少压缩后的体积 针对这…

Lua中..和...的使用区别

一. .. 的用法 二. ... 的用法 在 Lua 中&#xff0c;... 是一个特殊符号&#xff0c;它用于表示不定数量的参数。当你在函数定义或调用中使用 ... 时&#xff0c;它可以匹配任意数量的参数&#xff0c;并将它们作为列表传递。在您的代码示例中&am…

基于SSD的RAG技术方案,推动LLM规模扩展

随着大型语言模型&#xff08;LLM&#xff09;的不断发展&#xff0c;它们在虚拟助手、聊天机器人和对话系统等应用中发挥着重要作用。然而&#xff0c;LLM面临的挑战之一是它们可能会生成虚假或误导性的信息&#xff0c;即所谓的“幻觉”。为了解决这一问题&#xff0c;检索增…

Java数据库连接——JDBC

目录 1、JDBC简介 2、JDBC应用 2.1 建立数据库连接 2.1.1 DriverManager静态方法获取连接 2.1.2 DataSource对象获取 2.2 获取SQL执行对象 2.2.1 SQL注入 2.2.2 Statement(执行静态SQL) 2.2.3 PreparedStatement(预处理的SQL执行对象) 2.3 执行SQL并返回结果 2.4 关…

Error when custom data is added to Azure OpenAI Service Deployment

题意&#xff1a;在向 Azure OpenAI 服务部署添加自定义数据时出现错误。 问题背景&#xff1a; I receive the following error when adding my custom data which is a .txt file (it doesnt matter whether I add it via Azure Cognitive Search, Azure Blob Storage, or F…

证书学习(五)Java实现RSA、SM2证书颁发

目录 一、知识回顾1.1 X.509 证书1.2 X509Certificate 类二、代码实现2.1 Maven 依赖2.2 RSA 证书颁发1)PfxGenerateUtil 证书文件生成工具类2)CertDTO 证书中间类3)RSACertGenerateTest RSA证书生成测试类4)执行结果2.3 SM2 证书颁发1)SM2Utils 国密SM2算法工具类2)SM2C…