首页 > 系统相关 >【Linux修行路】多线程——互斥量

【Linux修行路】多线程——互斥量

时间:2024-09-04 16:25:13浏览次数:7  
标签:include lock Linux 互斥 mutex pthread 线程 多线程

目录

⛳️推荐

一、多线程模拟抢票

二、加锁——互斥量

2.1 pthread_mutex_init——初始化互斥量

2.2 pthread_mutext_destroy——销毁一个互斥量

2.3 pthread_mutex_lock——加锁

2.4 pthread_mutex_trylock——非阻塞的申请锁

2.4 pthread_mutex_unlock——解锁

2.5 定义一个全局或者静态的锁变量

2.6 加锁后的抢票

三、锁的原理

四、锁的封装


⛳️推荐

前些天发现了一个巨牛的人工智能学习网站,通俗易懂,风趣幽默,忍不住分享一下给大家。点击跳转到网站【Linux修行路】动静态库详解点击跳转到网站

一、多线程模拟抢票

#include <iostream>
#include <pthread.h>
#include <vector>
#include <cstdio>
#include <unistd.h>


using namespace std;

#define NUM 4

int tickets = 500; // 定义1000张票

class ThreaInfo
{
public:
    ThreaInfo(const string &threadname)
    :threadname_(threadname)
    {}

public:
    string threadname_;
};

void *GrabTickets(void *args)
{
    ThreaInfo *ti = static_cast<ThreaInfo*>(args);
    string name(ti->threadname_);
    while(true)
    {
        if(tickets > 0)
        {
            usleep(1000);
            printf("%s get a ticket: %d\n", name.c_str(), tickets);
            tickets--;
        }
        else break;
    }

    printf("%s quit...\n", name.c_str());
}

int main()
{
    vector<pthread_t> tids;
    vector<ThreaInfo*> tis;
    for(int i = 1; i <= NUM; i++)
    {
        pthread_t tid;
        ThreaInfo *ti = new ThreaInfo("Thread-"+to_string(i));
        pthread_create(&tid, nullptr, GrabTickets, ti);
        tids.push_back(tid);
        tis.push_back(ti);
    }

    // 等待所有线程
    for(auto tid : tids)
    {
        pthread_join(tid, nullptr);
    }

    // 释放资源
    for(auto ti : tis)
    {
        delete ti;
    }
    return 0;
}

image-20240313162028948

代码中大于0才抢票,但是最终有线程抢到的票号为负数,还有不同的线程抢到了同一张票,这显然是有问题的。

出现该现象的原因:tickets 属于可以被所有线程共享的共享数据。这种情况叫做,共享数据在无保护的情况下,被多线程并发访问,导致的数据不一致问题对一个全局变量进行多线程并发访问(--++)操作是不安全的

数据不一致的原因是: -- 操作不是原子的,一般来说,-- 会被解释成三条汇编,并且一个线程是可以随时被调度的,在执行这三条汇编中的任意一条时,都可能被调度。

image-20240313164141557

逻辑运算也要由 CPU 来执行,当 tickets 为 1 的时候,可能同时有多个线程来判断,线程 A 先来,将 tickets 在内存中的 1 加载到 CPU 的寄存器中,正准备进行逻辑运算的时候,线程 A 被切换出去了,寄存器中的 1 属于该线程的上下文数据,也会被切换出去,此时线程 B 被切换进来,还是先将内存中 tickets 的 1 加载到 CPU 的寄存器中,它运气比较好,一直执行完整个比较,发现还有一张票,会进行抢票操作,将票数减一,此时剩余票数为0,将 0 写回到内存。然后线程 B 被切换出去了,线程 A 被切换进来,A 线程将它的上下文数据恢复到 CPU 的寄存器中,也就是把 1 恢复到CPU 的寄存器中,进行比较,A 线程会认为还有票,接下来进行抢票,打印票数的时候,会重新去内存中读取 tickets 的值,此时内存中 tickets 的值已经被 B 线程修改成 0 了,所以 A 线程买到的就是 0 号票,此时已经有问题了,不可能存在 0 号票,接下来 A 线程对票数进行减减操作,还是先去内存中读取 tickets 的值,也就是 0, 然后对其进行减减操作,此时剩余票数就变成了 -1,然后将-1 写入到内存。这就是多线程对共享数据并发访问产生问题的具体过程。

二、加锁——互斥量

解决上面问题的方法就是加锁,Linux 上提供的这把锁叫做互斥量。**加锁的本质是:串行去执行临界区的代码,是用时间来换安全。**所以加锁的一个原则是:尽量的要保证临界区代码,越少越好。

2.1 pthread_mutex_init——初始化互斥量

#include <pthread.h>

int pthread_mutex_init(pthread_mutex_t *restrict mutex, const pthread_mutexattr_t
  • mutex:要初始化的互斥量。其中 pthread_mutex_t 是一个自定义数据类型,用来表示一个互斥量。

  • attr:锁的属性,一般设置为 nullptr

2.2 pthread_mutext_destroy——销毁一个互斥量

#include <pthread.h>

int pthread_mutex_destroy(pthread_mutex_t *mutex);

2.3 pthread_mutex_lock——加锁

#include <pthread.h>

int pthread_mutex_lock(pthread_mutex_t *mutex);

调用该函数可能出现的两种情况:

  • 互斥量处于未锁状态,该函数会将互斥量锁定,同时返回成功

  • 发起函数调用时,其它线程已经锁定互斥量,或者存在其它线程同时申请互斥量,但没有竞争到互斥量,那么该函数调用会陷入阻塞(执行六被挂起),等待互斥量解锁。

  • 不同线程对锁的竞争能力可能会不同,一个线程刚把锁释放,紧接着就立即去申请锁,那么该进程申请到锁的几率是比其它进程要大的。因为其它进程正处于被挂起的状态,要等待锁被释放,在锁被释放的时候,操作系统要先唤醒这些被挂起的进程,然后去申请锁,这个过程和前面那个一直在运行的进程相比一定是更慢的。在纯互斥环境中,如果锁分配不够合理,容易导致其他线程的饥饿问题(一个线程长时间申请不到互斥量)。因此我们需要想办法,让刚释放锁的线程不能再立即申请到锁。必须排在队伍的最后面。

    image-20240314082521012

    可能同时存在多个线程在等待一把锁资源,当这个锁被释放的时候,操作系统如果把所有等待的线程全部唤醒,这也是不合理的,因为最终只会有一个线程拿到锁资源。

    同步:让所有的线程获取锁,按照一定的顺序,按照一定的顺序性获取资源就叫同步。

    所有线程在执行临界区代码访问临界资源之前,都需要先申请锁,所以锁本身就是一种共享资源,这就决定了申请锁释放锁一定要被设计成原子性操作

    一个线程在执行临界区的代码时,是可以被切换的,在被切出去的时候,是持有锁被切出去的。所以在该线程释放锁资源之前,其它线程是无法进入临界区访问临界资源的。所以,当前线程访问临界区的过程,对于其它线程是原子的。

    2.4 pthread_mutex_trylock——非阻塞的申请锁

    int pthread_mutex_trylock(pthread_mutex_t *mutex);

申请成功返回0,失败错误码被返回。

2.4 pthread_mutex_unlock——解锁

#include <pthread.h>
 
int pthread_mutex_unlock(pthread_mutex_t *mutex);

2.5 定义一个全局或者静态的锁变量

pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;

对于全局和静态的锁,我们在使用的时候就不需要对其进行初始化和销毁。

2.6 加锁后的抢票

#include <iostream>
#include <pthread.h>
#include <vector>
#include <cstdio>
#include <unistd.h>


using namespace std;

#define NUM 4

int tickets = 500; // 定义1000张票

class ThreaInfo
{
public:
    ThreaInfo(const string &threadname, pthread_mutex_t *lock)
    :threadname_(threadname)
    ,lock_(lock)
    {}

public:
    string threadname_;
    pthread_mutex_t *lock_;
};

void *GrabTickets(void *args)
{
    ThreaInfo *ti = static_cast<ThreaInfo*>(args);
    string name(ti->threadname_);
    while(true)
    {
        pthread_mutex_lock(ti->lock_); // 加锁
        if(tickets > 0)
        {
            usleep(10000);
            printf("%s get a ticket: %d\n", name.c_str(), tickets);
            tickets--;
            pthread_mutex_unlock(ti->lock_); // 解锁
        }
        else 
        {
            pthread_mutex_unlock(ti->lock_); // 解锁
            break;
        }
        usleep(13); // 用休眠来模拟抢到票的后续动作
        // pthread_mutex_unlock(ti->lock_); // 不能在这里解锁,因为 tickets == 0 的时候就直接跳出循环了,导致锁没有被释放,其它线程就会阻塞住
    }

    printf("%s quit...\n", name.c_str());
}

int main()
{
    pthread_mutex_t lock; // 定义一个互斥量
    pthread_mutex_init(&lock, nullptr); // 初始化互斥量
    vector<pthread_t> tids;
    vector<ThreaInfo*> tis;
    for(int i = 1; i <= NUM; i++)
    {
        pthread_t tid;
        ThreaInfo *ti = new ThreaInfo("Thread-"+to_string(i), &lock);
        pthread_create(&tid, nullptr, GrabTickets, ti);
        tids.push_back(tid);
        tis.push_back(ti);
    }

    // 等待所有线程
    for(auto tid : tids)
    {
        pthread_join(tid, nullptr);
    }

    // 释放资源
    for(auto ti : tis)
    {
        delete ti;
    }

    pthread_mutex_destroy(&lock); // 销毁一个互斥量
    return 0;
}

多线程抢票

三、锁的原理

原子性:一件事情要么做了,要么没做,不存在中间状态,在计算机底层,一条汇编就是原子的。

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

image-20240314091050463

交换的本质是把内存中的数据 (我们定义的锁),交换到 CPU 的寄存器中,也就是把数据交换到线程硬件的上下文中,一个线程的硬件上下文属于一个线程的私有数据。所以锁本质上,就是把一个共享资源,让一个线程通过一条汇编指令,交换到自己的硬件上下文中。假设锁为1,每个线程都用0去交换,1只有一份,同一时刻就只有一个线程能够交换到1,一个线程交换到了1,就说明该线程申请锁成功。

解锁没有使用 exchange 而是使用 move,目的是,为了在一个线程申请到锁之后,可以由其它的线程去解锁。使用 exchange 就必须要求申请锁成功的线程去解锁,这样可能会导致死锁问题。

四、锁的封装

// 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_;
};
#include <iostream>
#include <pthread.h>
#include <vector>
#include <cstdio>
#include <unistd.h>
#include "LockGuard.hpp"

using namespace std;

#define NUM 4

int tickets = 500; // 定义1000张票

pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;

class ThreaInfo
{
public:
    ThreaInfo(const string &threadname /**, pthread_mutex_t *lock*/)
        : threadname_(threadname)
    /*,lock_(lock)*/
    {
    }

public:
    string threadname_;
    // pthread_mutex_t *lock_;
};

void *GrabTickets(void *args)
{
    ThreaInfo *ti = static_cast<ThreaInfo *>(args);
    string name(ti->threadname_);
    while (true)
    {
        {
            LockGuard lockguard(&lock); // RAII 风格的锁
            if (tickets > 0)
            {
                usleep(10000);
                printf("%s get a ticket: %d\n", name.c_str(), tickets);
                tickets--;
            }
            else
            {
                break;
            }
            // pthread_mutex_unlock(ti->lock_); // 不能在这里解锁,因为 tickets == 0 的时候就直接跳出循环了,导致锁没有被释放,其它线程就会阻塞住
        }
        usleep(13); // 用休眠来模拟抢到票的后续动作
    }

    printf("%s quit...\n", name.c_str());
}

int main()
{
    // pthread_mutex_t lock;
    // pthread_mutex_init(&lock, nullptr);
    vector<pthread_t> tids;
    vector<ThreaInfo *> tis;
    for (int i = 1; i <= NUM; i++)
    {
        pthread_t tid;
        ThreaInfo *ti = new ThreaInfo("Thread-" + to_string(i) /*, &lock*/);
        pthread_create(&tid, nullptr, GrabTickets, ti);
        tids.push_back(tid);
        tis.push_back(ti);
    }

    // 等待所有线程
    for (auto tid : tids)
    {
        pthread_join(tid, nullptr);
    }

    // 释放资源
    for (auto ti : tis)
    {
        delete ti;
    }

    pthread_mutex_destroy(&lock);
    return 0;
}

RAII风格:RAII(Resource Acquisition IInitialization)是由c++之父Bjarne Stroustrup提出的,中文翻译为资源获取即初始化,他说:使用局部对象来管理资源的技术称为资源获取即初始化;这里的资源主要是指操作系统中有限的东西如内存、网络套接字等等,局部对象是指存储在栈的对象,它的生命周期是由操作系统来管理的,无需人工介入。

标签:include,lock,Linux,互斥,mutex,pthread,线程,多线程
From: https://blog.csdn.net/m0_68662723/article/details/141872651

相关文章

  • ARM微处理器编程模型与linux驱动开发
    文章目录微处理器指令系统数据类型字节对符号位扩展ARM体系结构ARM处理器工作模式寄存器异常过程调用标准程序内存划分STM32的使用常用资源GPIO口的使用GPIO固件库的使用STM固件库Proteus常用元器件中断AD转换BootLoader的定制:嵌入式......
  • linux下安装jdk
     原文地址:https://www.cnblogs.com/caoyunpu/p/16660868.html 1、下载Linux版本的JDK(注意看自己安装的Linux系统是什么位数)查看本机位数命令:sudouname--m   JDK官网下载地址:https://www.oracle.com/java/technologies/downloads/#java18 2、使用工具远程进入Li......
  • Linux 安装nodejs环境
    文章目录Node.js简介Node.js的核心特性Node.js的生态系统Node.js的模块系统部署下载Node.js预编译二进制包上传到Linux服务器并解压配置环境变量验证安装部署在下边,我先对nodejs进行一些介绍,大家了解一下Node.js简介Node.js是一个基于ChromeV8引擎的JavaScript......
  • Linux中修改文件夹和子目录 所属的用户和用户组
    Linux下有几个命令可以用来更改文件或目录的属主(用户)和属组(组):1.chown命令:用于更改文件或目录的属主。它的基本语法是:“`chown<新属主><文件或目录>chownuser1file.txt这样就将file.txt的属主更改为user1。 #把testFolder文件夹和子目录所属的用户,用户组做修改c......
  • 北京迅为RK3568开发板嵌入式学习之Linux驱动全新更新-CAN+网络设备
    iTOP-3568开发板采用瑞芯微RK3568处理器,22nm工艺制程,集成4核A55架构处理器和MaliG522EE图形处理器,支持4K解码和1080P编码,内置独立的NPU,可用于轻量级人工智能应用。 iTOP-3568开发板连接器版本:  iTOP-3568开发板邮票版本:     关注:北京迅为电子,在线观看 ---......
  • Linux之tcpdump
    参数详解参数含义-i指定tcpdump监听的网络接口eth0bond0等等-c指定要监听的数据包数量,达到指定数量后自动停止抓包-w指定将监听到的数据包写入文件中保存-A指定将每个监听到的数据包以ACSII可见字符打印-n指定将每个监听到数据包中的域名转换成IP......
  • 【Linux】进程间的关系(第十三篇)
    目录1.亲缘关系:2.进程组关系:3.会话关系4.进程、进程组与会话的关系5.例子1.亲缘关系:2.进程组关系:3.进程间会话关系1.亲缘关系:多个进程间可能存在亲缘关系(多个进程间可能是父子进程结构,也可能更为复杂的层级亲缘结构)2.进程组关系:定义:进程组是一个或多个进程的集......
  • 【Linux】孤儿进程(第十二篇)
    目录孤儿进程定义产生原因处理机制特性与影响示例守护进程(daemon)定义:特点:与孤儿进程的区别:孤儿进程孤儿进程是操作系统中的一个概念,主要出现在类UNIX操作系统中。以下是关于孤儿进程的详细解释:定义孤儿进程指的是在其父进程执行完成或被终止后,仍继续运行的......
  • MYSQL-Linux安装
    MySQL8.0.26-Linux版安装1.准备一台Linux服务器云服务器或者虚拟机都可以;Linux的版本为Centos7;2.下载Linux版MySQL安装包https://downloads.mysql.com/archives/community3.创建目录、解压mkdirmysq1//创建目录tar-xvfmysql安装包.x8664.rpm-bundle.tar-Cmysql/......
  • Linux keepalive
    安装1,安装 https://www.cnblogs.com/lfxx/p/17876757.htmlhttps://www.cnblogs.com/wangchengshi/p/10912177.html 2,linuxkeepalived日志,如何重定向Keepalived日志的输出路径https://blog.csdn.net/weixin_39935571/article/details/116731816https://www.......