当前位置: 首页 > article >正文

Linux_线程互斥

互斥的相关概念

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

一个不加保护的Demo

这里使用多个线程共同执行上面的方法,代码很简单。但是运行结果怎么出现了负数?等于0不就直接break了吗?

原因有以下两点

  • 代码if(XXXX)不是原子操作,ticketnum--也不是原子操作
  • 所有的线程在尽可能多的进行调度切换执行 --- 线程或者进程什么时候会切换?
    • a.时间片耗尽
    • b.更高优先级的进程要调度
    • c.通过sleep,从内核返回用户时,会进行时间片是否到达的检测,进而导致切换

当我们执行上述代码时,每个线程都要这样执行上面的逻辑,但cpu的寄存器只有一套,但是寄存器中的数据有多套,且数据为线程私有。由于ticketnum--操作不是原子的(即,将ticketnum的值移动到CPU,CPU做运算,再将结果写回内存。共三步)。当一个线程正走到以上逻辑的第二步时,正准备判断,此时这个线程被切换了,一旦被切换,当前线程在寄存器中数据都会保存下来,等在被切回来的时候,再恢复!

        当票数为1时,a线程会做判断,符合逻辑进入if,走到usleep语句;此时b线程也进来了,a将寄存器中的数据带走,此时b线程见到的票数也是1,b线程也符合逻辑,进入if,也会走到usleep;同样的c和d线程都会做以上线程的动作,都会进入if。当a过了usleep时间,会执行--操作(1.重读数据2.--数据3.写回数据),此时票数为0了,同样的b,c,d线程也会做--,因为它们已经进入了if中。最后就导致票数为-2的情况了。

互斥量mutex

在Linux中互斥量就是锁

要解决上述多线程并发引起的安全问题,我们只需在进入临界区之前加上一把锁,就可以完美解决。

 互斥量(锁)的相关接口

  • pthread_mutex_init: 初始化互斥锁。
int pthread_mutex_init(pthread_mutex_t *restrict mutex, const pthread_mutexattr_t *restrict attr);

pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;// 使用宏值初始化(全局)

    mutex:指向要初始化的互斥锁对象的指针。

    attr:指定互斥锁属性的对象,如果传递NULL,则使用默认的互斥锁属性。

    pthread_mutex_init 函数若调用成功,会返回 0。若发生错误,会返回一个非零的错误码。

    • pthread_mutex_destroy: 销毁互斥锁。
    int pthread_mutex_destroy(pthread_mutex_t *mutex);
    
    • pthread_mutex_lock: 锁定互斥锁
    int pthread_mutex_lock(pthread_mutex_t *mutex);
    
    • pthread_mutex_unlock: 解锁互斥锁。
     int pthread_mutex_unlock(pthread_mutex_t *mutex);
    

    锁接口的使用

    全局锁

    // 定义一个全局锁
    pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;
    int ticketnum = 10000; // 共享资源,临界资源
    
    void Ticket()
    {
        while (true)
        {
            pthread_mutex_lock(&lock); // 加锁
            if (ticketnum > 0)
            {
                usleep(1000);
                printf("get a new ticket, id: %d\n", ticketnum--);
                pthread_mutex_unlock(&lock);     // 解锁
            }
            else{
                pthread_mutex_unlock(&lock);     // 解锁
                break;
            }
        }
    }
    int main()
    {
        // 创建多线程的逻辑,调用Tichet
        return 0;
    }

     局部锁

    使用ThreadData接收参数,包括锁的接收,保证每一个线程都能看到同一把锁

    #include <iostream>
    #include <string>
    #include <pthread.h>
    #include <functional>
    #include <sys/types.h>
    #include <unistd.h>
    
    namespace ThreadModule
    {
        // 要传递的参数
        struct ThreadData
        {
            ThreadData(const std::string &name, pthread_mutex_t *lock_ptr)
                : _name(name)
                , _lock_ptr(lock_ptr)
            {}
            std::string _name;
            pthread_mutex_t *_lock_ptr;
        };
    
        // 执行任务的方法
        using func_t = std::function<void(ThreadData*)>;
    
        // 线程状态-枚举
        enum class TSTATUS
        {
            NEW,
            RUNNING,
            STOP
        };
    
        class Thread
        {
        private:
            // 成员方法,具备this指针,置为static之后就不具备this指针了
            static void *Routine(void *args)
            {
                // t就拿到了this指针
                Thread *t = static_cast<Thread *>(args);
                t->_status = TSTATUS::RUNNING;
                t->_func(t->_td); // 就可以执行相应的类内方法了
                return nullptr;
            }
    
        public:
            // 线程要执行的方法直接传进来
            Thread(const std::string &name, func_t func, ThreadData* td)
                : _name(name)
                , _func(func)
                , _td(td)
                , _status(TSTATUS::NEW)
                , _joinable(true)
            {}
            bool Start()
            {
                if (_status != TSTATUS::RUNNING)
                {
                    int n = ::pthread_create(&_tid, nullptr, Routine, this); // 将this指针通过参数传过去
                    if (n != 0)
                        return false;
                    return true;
                }
                return false;
            }
            bool Stop()
            {
                if (_status == TSTATUS::RUNNING)
                {
                    int n = ::pthread_cancel(_tid);
                    if (n != 0)
                        return false;
                    _status = TSTATUS::STOP;
                    return true;
                }
                return false;
            }
            bool Join()
            {
                if (_joinable)
                {
                    int n = ::pthread_join(_tid, nullptr);
                    if (n != 0)
                        return false;
                    _status = TSTATUS::STOP;
                    return true;
                }
                return false;
            }
            std::string Name() { return _name; }
            ~Thread() {}
    
        private:
            std::string _name; // 线程名字
            pthread_t _tid;    // 线程id
            bool _joinable;    // 是否是分离状态,默认不是
            func_t _func;      // 线程未来要执行的方法
            TSTATUS _status;   // 线程状态
            ThreadData* _td;   // 要传递的参数
        };
    }
    

    让每个线程都获取局部锁的地址,在每个线程在执行抢票逻辑的时候,将锁的地址传给加锁函数,就能实现局部加锁了。 

    #include "Thread.hpp"
    #include <vector>
    
    int ticketnum = 10000;
    void Ticket(ThreadModule::ThreadData *td)
    {
        while(true)
        {
            pthread_mutex_lock(td->_lock_ptr);      // 加锁
            if(ticketnum > 0)
            {
                // 抢票
                printf("get a new ticket, who get it: %s, id: %d\n", td->_name.c_str(), ticketnum--);
                pthread_mutex_unlock(td->_lock_ptr);// 解锁
            }
            else
            {
                pthread_mutex_unlock(td->_lock_ptr);// 解锁
                break;
            }
        }
    }
    #define NUM 4
    int main()
    {
        // 创建局部锁
        pthread_mutex_t mutex;
        pthread_mutex_init(&mutex, nullptr);
        // 创建线程对象
        std::vector<ThreadModule::Thread> threads;
        for(int i = 0;i < NUM; i++)
        {
            std::string name = "thread-" + std::to_string(i+1);
            // 把锁的地址给到td对象
            ThreadModule::ThreadData *td = new ThreadModule::ThreadData(name, &mutex);
            // 之后在将td给到Thread
            threads.emplace_back(name, Ticket, td);
        }
        // 启动线程
        for(int i = 0; i< NUM;i++)
            threads[i].Start();
        // 等待线程
        for(int i = 0; i< NUM;i++)
            threads[i].Join();
        // 释放锁
        pthread_mutex_destroy(&mutex);  
        return 0;
    }

    锁的相关问题

    1. 锁本身是全局的,那么锁也是共享资源!谁保证锁的安全?

            pthread_mutex:加锁和解锁被设计成为原子的了

    2. 如何看待锁呢?二元信号量就是锁!

            2.1 加锁本质就是对资源展开预订!

            2.2 整体使用资源!!

    3. 如果申请锁的时候,锁被别人已经拿走了,怎么办?

            其他线程要进行阻塞等待

    4. 线程在访问临界区代码的时候,可以不可以切换?可以切换!!

            4.1 我被切走的时候,别人能进来吗?不能!因为我是抱着锁,被切换的!临界区的代码就是被串行的!这也是加锁效率低的原因!也体现了原子性(要么不做,要么做完)!

    锁是如何实现的

    现在大家已经意识到单纯的 i++ 或者 ++i 都不是原子的,有可能会有数据一致性问题。
    在内核中,为了实现互斥锁操作,大多数体系结构都提供了swap或exchange指令,该指令的作用是把寄存器和内存单元的数据相交换,由于只有一条指令,保证了原子性,即使是多处理器平台,访问内存的 总线周期也有先后,一个处理器上的交换指令执行时,另一个处理器的交换指令只能等待总线周期。 

    将%al看成一个寄存器,把 0 movb到 %al中,xchgb将内存中的变量与寄存器中的做了直接的交换,不需要中间变量。(我们假定mutex一开始的数据是:1表示锁没有被申请;0表示锁被申请了)。线程执行判断,如果%al中的内容>0,则申请锁成功然后返回,否则挂起等待,等待完成被唤醒,goto lock重新申请锁。

    1. CPU的寄存器只有一套,被所有的线程共享。但是寄存器中的数据,属于执行流上下文,属于执行流私有的数据!
    2. CUP在执行代码的时候,一定要有对应的执行载体 -- 线程&&进程。
    3. 数据在内存中,是被所有线程所共享的

    结论:把数据从内存移动到寄存器,本质是把数据从共享,变成线程私有!


    重新理解加锁

    当线程A执行第一行代码时,此时%al寄存器中为0,内存mutex中为1(图1);执行第二条代码时内存中mutex中的数据与%al进行交换,变为%al中值为1,mutex的值为0(图2);我们假设线程A执行第三行代码的时候被切换走,线程A会保存自身的上下文,带走%al中的数据,此时线程A处在第三行。

            这时线程B来了,并且开始走第一行和第二行代码,由于内存中mutex的值为0(还是处于图2的状态),交换之后%al的值还是0。所以当线程B执行到第3行代码的时候只能跳到第6行,进行挂起等待。

            线程B被挂起,线程A被重新切回,并恢复上文数据,从第三行开始执行,进入if,调用接口pthread_mutex_lock,return 0表示加锁成功,进入临界区。所以此时线程A被称为:申请锁成功。在上面代码中,加锁就是执行第二行代码:xchgb,只有一条汇编代码,交换不是拷贝,只有一个“1”,持有1的,就表示持有锁!

            当线程A执行完临界区的代码后,进行解锁,执行第八行代码,将自身持有的“1”movb到内存中(这样就回到了图1的状态),接着唤醒正在等待mutex的线程B,线程B被唤醒后,执行第七行代码,继续goto lock重新申请锁。

    原文地址:https://blog.csdn.net/2302_77644537/article/details/145358245
    本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.kler.cn/a/524275.html

    相关文章:

  • 【ENSP】链路聚合的两种模式
  • SpringBoot+微信小程序+数据可视化的宠物到家喂宠服务(程序+论文+讲解+安装+调试+售后等)
  • chrome://version/
  • Anaconda +Jupyter Notebook安装(2025最新版)
  • 基于CanMV IDE 开发软件对K210图像识别模块的开发
  • Typora“使用”教程
  • Git 仓库命令
  • 58.界面参数传递给Command C#例子 WPF例子
  • WordPress Icegram Express插件Sql注入漏洞复现(CVE-2024-2876)(附脚本)
  • Java 大视界 -- Java 大数据在自动驾驶中的数据处理与决策支持(68)
  • 安卓逆向之脱壳-认识一下动态加载 双亲委派(一)
  • 设计模式的艺术-观察者模式
  • (done) ABI 相关知识补充:内核线程切换、用户线程切换、用户内核切换需要保存哪些寄存器?
  • MATLAB中extractAfter函数用法
  • Git进阶之旅:Git 命令
  • Django ORM解决Oracle表多主键的问题
  • 全程Kali linux---CTFshow misc入门(1-12)
  • CMake常用命令指南(CMakeList.txt)
  • Vue 3 30天精进之旅:Day 07 - Vue Router
  • 【Python百日进阶-Web开发-FastAPI】Day812 - FastAPI Cookie 参数、Header 参数
  • 运用python爬虫爬取汽车网站图片并下载,几个汽车网站的示例参考
  • 一个python项目中的文件和目录的作用是什么?scripts,venv,predict的具体含义
  • GO 高级特性篇
  • 常见端口的攻击思路
  • 爱书爱考平台说明
  • C#操作GIF图片(上)