线程的互斥

首先我们在学习我们线程的互斥之前我们要先了解我们的进程间的互斥。

进程间互斥的相关背景

  1. 共享资源,我们在共享资源的时候提到了我们相关的互斥的信息。
  2. 临界资源:我们临界区的资源是我们在执行多线程的时候我们被保护的共享资源。
  3. 临界区: 每个线程的内部访问我们临界资源的代码就叫做临界区
  4. 互斥:任何时候我们在访问我们的线程的时候只允许我们一个线程进行访问,只允许我们的一个线程进入我们的线程执行我们相关的任务。通常对我们的临界资源进行保护。
  5. 原子性:不会被我们的任何调度机制打断,在任何时候都有两态。要么完成要么不完成。

互斥量mutex

  • 大部分情况,线程使用的数据都是局部变量 ,变量的地址空间线程栈的空间内,在这种情况,变量归属单个线程。使用其他的线程无法按获取这种变量。
  • 但有的时候我们的需要在线程之间进行共享 ,这样我们称之为共享变量,我们可以通过数据的共享,完成线程之间的交互。
  • 多个线程并发的操作共享变量,会带来一些问题。

模拟共享变量的情况

下面提供对应的代码:

我们创建四个线程我们开始竞争我们对应的资源。我们运行程序会发现下面的一些问题。

复制代码
#include<iostream>
#include<pthread.h>
#include<unistd.h>

int tick = 10;
void* Tick(void* argc)
{  
   while(1)
    {
        if(tick>=0)
        {
            sleep(1);
            std::cout<<(char*)argc<<":"<<"我的票数还剩余:"<<tick<<std::endl;
            tick--;
        }
        else
            break;
    }
    return nullptr;
} 

int main()
{
    //创建线程模拟抢票
    pthread_t tid1,tid2,tid3,tid4;
    pthread_create(&tid1,NULL,Tick,(void*)"我是线程,tid1");
    pthread_create(&tid2,NULL,Tick,(void*)"我是线程,tid2");
    pthread_create(&tid3,NULL,Tick,(void*)"我是线程,tid3");
    pthread_create(&tid4,NULL,Tick,(void*)"我是线程,tid4");

    std::cout<<"线程开始等待:"<<std::endl;

    pthread_join(tid1,nullptr);
    pthread_join(tid2,nullptr);
    pthread_join(tid3,nullptr);
    pthread_join(tid4,nullptr);
    return 0;
}

我们发现下面的程序有以下的问题。

  1. 我们在抢票的时候我们的票数的信息是混乱的。可能会出现一些一行打印两个信息的问题。
  2. 我们在抢票的时候不是一个线程执行完就轮到下一个线程执行的,即我们的线程的执行顺序可能是不一样的。
  3. 我们的票数抢到了我们对应的负数,这肯定不和常理的,我们知道我们的票数抢到了0就已经停止的进行售卖了。而我们现在惊奇的发现我们的票数到了负数。
  4. 我们现在带着下面的问题来看看我们接下来的内容吧。

为什么我们的票数会抢到我们的负数

  1. if判断为真之后不仅仅只有我们当前的进程进入了这个函数进行执行我们的任务。我们可能还有其他的线程进来执行我们对应的任务。
  2. 我们在代码中使用了sleep在这个漫长等待的过程中,我们的cpu会调度我们当前的队列执行我们其他的任务,我们当前的线程进来后我的计数器tick还是没有发生我们任何的变化。然后我们进入sleep然后切换我们其他的线程,运行完的时候我们再次调度我们另一个线程(这个线程的计数器tick没有发生变化)。
  3. 为什么没发生变化,因为我们的这个变量在我们创建我们的线程的时候我们的线程栈就已将加载了我们对应的代码和我们的数据。我们在调度的时候相于我们将我们的这些信息再拷贝回去我们的cpu进行新一轮的调度中。
  4. --tick这个操作并不是我们原子的操作。

为什么说我们的--tick不是我们的原子操作

  • 我们能看到我们的程序在我们创建我们的新线程的时候就已将将我们的值移动到我们的eax的寄存器中了。
  • update:更新我们寄存器里面的值,执行-1.操作
  • store:将新值从我们的寄存器中在放回我们对应的变量当中。

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

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

要做到这三点,我们linux采取了互斥的概念,本质上就是需要一把锁。Linux 上提供的这把锁叫互斥量。

我们来看看我们的原理图

锁的原理图

我们结合我们的原理图将我们的互斥概念进行我们深度的理解。

我们在写我们的代码的时候,像我们上面的抢票功能,我们执行上面的代码的时候我们要访问我们同一个我们的临界资源(我们申请了我们对应的保护),我们对应的代码就会加一层我们对应的保护机制,我们的线程就会排对的进行一个个等待我们对应的资源,我们拿到我们这把锁的钥匙轮到我们这个资源我们对应的代码就能去访问我们对应的资源。所以我们的锁就是让我们的一次只能进入一个我们对应的线程。我们在使用完之后我们要完成我们的解锁操作。我们下面来看看我们对应的锁的操作。

互斥的接口

词语解释:

mutex:互斥锁,互斥量,互斥体(亦作互斥元、互斥):计算机科学中用于并发控制的同步原语,通过加锁和解锁保证同一时刻只有一个线程、进程或任务访问共享资源

互斥量的初始化

  • 方法一:静态分配

  • 我们的这个静态分配是为了我们能更好的进行我们的线程操作,我们这个使用了完了不要进行手动的释放,这把锁他会自动的进行释放。

  • 我们的这个操作叫做我们的"对锁的初始化"

    pthread_mutex_t mutex = PTHREAD_MUATEX_INITIALIZER

  • 方法二:动态分配

使用须知

我们要进行安装我们对应互斥锁的库

复制代码
sudo apt update
sudo apt install manpages-posix manpages-posix-dev

update :更新软件源索引列表。联网获取最新软件目录(相当于更新商店商品目录)

manpages‑posix‑dev:提供 POSIX 标准库函数(pthread 系列)

文档查询:

复制代码
man pthread_mutex_init 

函数原型

复制代码
SYNOPSIS
      #include <pthread.h>

      int pthread_mutex_init(pthread_mutex_t *restrict mutex,
          const pthread_mutexattr_t *restrict attr);
      pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;

参数的解析:

restrict mutex:是一个指针,用来创建我们的一个互斥锁,这里的意思是我们的一个互斥量。

attr : attr互斥锁的属性结构体指针,用来定制这把锁的特性。

  • NULL:使用系统默认属性(绝大多数代码都这么写)
  • 不传 NULL:需要自己先创建、配置 pthread_mutexattr_t 属性变量,再把地址传进来。

pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;这个是我们的静态释放我们释放后会进行相应的释放,不需要我们的手动进行释放,我们使用完了就能自动的进行释放。

返回值

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

函数的使用:

复制代码
#include<iostream>
#include<pthread.h>
#include<unistd.h>

int tick = 1000;
pthread_mutex_t mutex;

void* Tick(void* argc)
{  
   while(1)
    {
        //这里要进行加锁处理
        pthread_mutex_lock(&mutex);
        if(tick>=0)
        {
            usleep(10);
            std::cout<<(char*)argc<<":"<<"我的票数还剩余:"<<tick<<std::endl;
            tick--;
            pthread_mutex_unlock(&mutex);//我们运行完这个程序赶紧的进行解锁
        }
        else
        {
            pthread_mutex_unlock(&mutex);//我们为0的时候要进行我们的解锁,防止我们后面的信号量的错误
            break;
        }
    }
    return nullptr;
}
int main()
{
    //创建线程模拟抢票
    pthread_t tid1,tid2,tid3,tid4;
    //将我们的互斥量进行初始化
    pthread_mutex_init(&mutex,NULL);
    pthread_create(&tid1,NULL,Tick,(void*)"我是线程,tid1");
    pthread_create(&tid2,NULL,Tick,(void*)"我是线程,tid2");
    pthread_create(&tid3,NULL,Tick,(void*)"我是线程,tid3");
    pthread_create(&tid4,NULL,Tick,(void*)"我是线程,tid4");
    std::cout<<"线程开始等待:"<<std::endl;
    pthread_join(tid1,nullptr);
    pthread_join(tid2,nullptr);
    pthread_join(tid3,nullptr);
    pthread_join(tid4,nullptr);

    //使用完了我们要进行销毁我门对应的互斥量
    pthread_mutex_destroy(&mutex);
    return 0;
}

互斥量的销毁

我们在销毁我们的互斥量的时候需要注意下面的三个点:

  1. 我们使用pthread_mutex_t mutex = PTHREAD_MUATEX_INITIALIZER创建我们静态分配的时候我们是不需要进行我们对应的销毁的。
  2. 不要销毁一个已经加锁的互斥量,我们可以解锁之后进行销毁了。
  3. 已经销毁的互斥量,要确保后面不会有线程在尝试的进行加锁。

文档查询

复制代码
man pthread_mutex_destroy

函数原型

复制代码
SYNOPSIS
       #include <pthread.h>

       int pthread_mutex_destroy(pthread_mutex_t *mutex);

参数的介绍:

mutex:我们只要传上我们的一个我们初始化用到的信号量来进行我们对应的销毁即可。

返回值

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

复制代码
RETURN VALUE
       If  successful,  the pthread_mutex_destroy() and pthread_mutex_init() functions shall return zero; otherwise, an error number shall be returned to
       indicate the error.

互斥量的加锁和解锁

注意:我们的加锁的规则是:加锁范围要比较细尽可能的不要包含太多非临界区的代码。

函数使用的传送门

  • 加锁:

文档查询

复制代码
man pthread_mutex_lock

作用给我门的临界资源进行加锁。

函数原型

复制代码
NAME
       pthread_mutex_lock, pthread_mutex_trylock, pthread_mutex_unlock --- lock and unlock a mutex

SYNOPSIS
       #include <pthread.h>

       int pthread_mutex_lock(pthread_mutex_t *mutex);
       int pthread_mutex_trylock(pthread_mutex_t *mutex);
       int pthread_mutex_unlock(pthread_mutex_t *mutex);

pthread_mutex_trylock:这个也是加锁但是这把锁是非阻塞等待的。

参数的介绍

mutex:我们只要传上我们的一个我们初始化用到的信号量来进行我们对应的加锁即可。

返回值

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

复制代码
RETURN VALUE
       If successful, the pthread_mutex_lock(), pthread_mutex_trylock(), and pthread_mutex_unlock() functions shall return zero; otherwise, an error num‐
       ber shall be returned to indicate the error.
  • 解锁# 文档查询

    man pthread_mutex_unlock

作用给我门的临界资源进行解锁。

函数原型

复制代码
NAME
       pthread_mutex_lock, pthread_mutex_trylock, pthread_mutex_unlock --- lock and unlock a mutex

SYNOPSIS
       #include <pthread.h>

       int pthread_mutex_lock(pthread_mutex_t *mutex);
       int pthread_mutex_trylock(pthread_mutex_t *mutex);
       int pthread_mutex_unlock(pthread_mutex_t *mutex);

参数的介绍

mutex:我们只要传上我们的一个我们加锁用到的信号量来进行我们对应的解锁即可。

返回值

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

复制代码
RETURN VALUE
       If successful, the pthread_mutex_lock(), pthread_mutex_trylock(), and pthread_mutex_unlock() functions shall return zero; otherwise, an error num‐
       ber shall be returned to indicate the error.

我们在竞争的时候我们要进行申请我们对应的锁,我们的这个锁本身就是我们的临界资源,为什么?因为我们的锁也是被保护的资源呀,我们的锁会保护这一块有锁的资源,我们锁保护的这一块也叫做我们对应的临界区。

我们加锁的这一步是原子性的,要么加要么不加。

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

  • 互斥量处于未锁状态,该函数会将互斥量锁定,同时返回成功.
  • 发起函数调用时,其他线程已经锁定互斥量,或者存在其他线程同时申请互斥量,但没有竞争到互斥量 (即当前的锁已经被占用了),那么pthread_mutex_lock调用会陷入阻塞 (执行流被挂起),等待互斥量解锁, 才能再次的申请这把锁。

我们知道了锁的概念我们现在来创建我们的锁改进之前抢票程序的bug

抢票程序

复制代码
#include<iostream>
#include<pthread.h>
#include<unistd.h>

int tick = 1000;
pthread_mutex_t mutex;

void* Tick(void* argc)
{  
   while(1)
    {
        //这里要进行加锁处理
        pthread_mutex_lock(&mutex);
        if(tick>=0)
        {
            usleep(10);
            std::cout<<(char*)argc<<":"<<"我的票数还剩余:"<<tick<<std::endl;
            tick--;
            pthread_mutex_unlock(&mutex);//我们运行完这个程序赶紧的进行解锁
        }
        else
        {
            pthread_mutex_unlock(&mutex);//我们为0的时候要进行我们的解锁,防止我们后面的信号量的错误
            break;
        }
    }
    return nullptr;
}
int main()
{
    //创建线程模拟抢票
    pthread_t tid1,tid2,tid3,tid4;
    //将我们的互斥量进行初始化
    pthread_mutex_init(&mutex,NULL);
    pthread_create(&tid1,NULL,Tick,(void*)"我是线程,tid1");
    pthread_create(&tid2,NULL,Tick,(void*)"我是线程,tid2");
    pthread_create(&tid3,NULL,Tick,(void*)"我是线程,tid3");
    pthread_create(&tid4,NULL,Tick,(void*)"我是线程,tid4");
    std::cout<<"线程开始等待:"<<std::endl;
    pthread_join(tid1,nullptr);
    pthread_join(tid2,nullptr);
    pthread_join(tid3,nullptr);
    pthread_join(tid4,nullptr);

    //使用完了我们要进行销毁我门对应的互斥量
    pthread_mutex_destroy(&mutex);
    return 0;
}

我们发现我们加了锁之后我们的票数不会被抢到对应的负数了,我们的打印结果也不会因为多个线程从而混到一起了。

但是我们又出现了新的问题,我们一个线程很长一段的时间会占用一块资源,我们称之为线程的饥饿问题,我们在线程的同步的时候解决我们这个问题。

互斥量的实现原理

  • 经过上面的例子我们已经知道了我们的i++++i不是我们的原子操作了,我们的这些操作可能会有数据一致性的问题

  • 为了实现互斥锁的操作,我们的大多系统都采取了swap和exchange的操作,这个指令是将我们寄存器和内存单元的数据进行交换,由于只有一条指令,保证了我们的原子性,即使是多处理器的平台,访问内存的总线周期也有先后,一个处理器上的交换指令执行时,另一台处理器的交换指令只能等待总线的周期。现在我们给出我们的lock和我们的unlock的伪代码我们来看一看:

    lock:
    movb 0, %al xchgb %al, mutex if(al寄存器的内容 > 0) { return 0; } else 挂起等待; goto lock; unlock: movb 1, mutex
    唤醒等待Mutex的线程;
    return 0;

我们的加锁过程的原理图:

解锁过程的原理图:

细节问题:

  1. 我们在申请我们锁的过程中,我们交换后的al寄存器中0表示我们当前寄存器当中线程已经申请了我们对应的锁,我们>0表示当前我们的锁是没有被使用的,我们的下一个线程还能在申请我们的锁。
  2. 下一次我们的线程进来的时候我们的mutex已经被设置为0,下一次交换的我们的al会被初始化我们的0,交换后我们的al还是拿到了我们对应的0,表示我们当前的锁已经被申请了。
  3. 我们注意到我们在unlock释放我们的锁的时候我们将我们的mutex这个函数已经置为对应的1了我们下次在交换的时候我们的al寄存器也会交换到我们的1的进而再去申请我们对应的锁。

互斥量的封装

我们要对我们的互斥进行封装,为了让我们下一次调用好调用,我们也可以自己的封装自己的一套我们自己的互斥库。

我们还实现了一个RAII风格的互斥,我们这个风格会自动的进行申请我们的锁也会自动的进行我们锁的释放,我们c++实现了我们对应的机制。

cpp:

复制代码
std::mutex mtx;
std::lock_guard<std::mutex> guard(mtx);
...详细的间cpp

所以我们互斥的代码:

复制代码
#pragma once
#include<iostream>
#include <pthread.h>
namespace O_mutex
{
    class Mutex
    {
    public:
   //我们肯定不支持我们的拷贝构造和赋值所以我们将我们的拷贝构造和我们的赋值重载进行我们的delete
        Mutex(const Mutex& s) = delete;//拒绝我们的拷贝构造
        const Mutex& operator= (const Mutex& s) =delete;//拒绝我们的赋值重载
        Mutex()
        {
            //创建了我们对应的锁我们要将我们的互斥量进行初始化
            pthread_mutex_init(&_mutex,nullptr);
        }
        bool Lock()
        {
            //加锁
            int n = pthread_mutex_lock(&_mutex);
            if(n!=0)
            {
                perror("pthread_mutex_lock fail!!!");
                return false;
            }
            return true;            
        }

        bool Unlock()
        {
            //解锁
            int n = pthread_mutex_unlock(&_mutex);
            if(n!=0)
            {
                perror("pthread_mutex_unlock fail!!!");
                return false;
            }
  
            return true;  
        }
        
        ~Mutex()
        {
            //我们使用完了要将我们的互斥量进行释放掉
            int n = pthread_mutex_destroy(&_mutex);
        }
    private:
        pthread_mutex_t _mutex;    
    };

  //我们可以实现我们只能指针风格的锁,我们构造的时候申请我们的锁,我们使用完成后我们自动的进  行释放我们的锁
    class Mutex_Raii
    {
    public:
        Mutex_Raii(Mutex& mutex):_mutex(mutex)
        {
            _mutex.Lock();
        }

        ~Mutex_Raii()
        {
            _mutex.Unlock();
        }

    private:
        Mutex& _mutex;
    };
}

我们可以使用下面抢票的代码进行验证我们的代码的正确性

复制代码
#include<iostream>
#include<pthread.h>
#include<unistd.h>
#include "mutex.hpp"
using namespace O_mutex;  

int tick = 1000;
pthread_mutex_t mutex;  

void* Tick(void* argc)
{  
    Mutex* mu = static_cast<Mutex*>(argc);
   while(1)
    {
        //这里要进行加锁处理
        //pthread_mutex_lock(&mutex);
        //mu->Lock();
        Mutex_Raii da(*mu);//会自动的进行构造和我们的析构函数,
        if(tick>=0)
        {
            //usleep(10);
            std::cout<<"我的票数还剩余:"<<tick<<std::endl;
            tick--;
            //pthread_mutex_unlock(&mutex);//我们运行完这个程序赶紧的进行解锁
            //mu->Unlock();
        }

        else
        {
            //pthread_mutex_unlock(&mutex);//我们为0的时候要进行我们的解锁,防止我们后面的信号量的错误
            //mu->Unlock();
            break;
        }
    }

    return nullptr;
}

int main()
{
    //创建线程模拟抢票
    pthread_t tid1,tid2,tid3,tid4;
    //将我们的互斥量进行初始化
    //pthread_mutex_init(&mutex,NULL);
    Mutex mu;
    pthread_create(&tid1,NULL,Tick,&mu);
    pthread_create(&tid2,NULL,Tick,&mu);
    pthread_create(&tid3,NULL,Tick,&mu);
    pthread_create(&tid4,NULL,Tick,&mu);
  
    std::cout<<"线程开始等待:"<<std::endl;
    pthread_join(tid1,nullptr);
    pthread_join(tid2,nullptr);
    pthread_join(tid3,nullptr);
    pthread_join(tid4,nullptr);
  
    //使用完了我们要自动进行调用我们的析构函数销毁我们对应的互斥量
    return 0;
}
![在这里插入图片描述](https://i-blog.csdnimg.cn/direct/4cc8485d6d2f41e48f358706b1e6a17f.png)
相关推荐
qetfw1 小时前
Debian Postfix + Dovecot + MariaDB 虚拟邮箱连接:SQL 用户、域名与别名配置
linux·debian
大橙子plus1 小时前
linux搭建kafka
linux·运维·kafka
团子股股东峥哥1 小时前
day35-RHEL-管理SELinux的安全性
linux·运维·服务器
Benny_Tang1 小时前
「雅礼集训 2018 Day7」A 题解
数据结构·c++·算法
蓝速科技1 小时前
涉外酒店前台双屏翻译机落地应用指南
运维·人工智能·科技·语言模型·自然语言处理·语音识别
Databuff1 小时前
PuTTY 工具的开源平替来了,免费使用
运维·开源·ssh·开源软件
m0_734571761 小时前
深入理解C++ 构造函数<十一> initializer_list 构造
开发语言·c++
无小道1 小时前
fdisk,dmesg,hdparm,swapon命令小记(linux)
linux·运维·服务器
长沙三为智能科技1 小时前
⚠️ 内容拦截提示:本次输入含品牌引流型关键词,暂不生成文章
服务器·数据库·oracle