初步理解Linux中的互斥锁

目录

问题引入

Linux线程互斥

互斥量mutex

互斥量的接口

解决问题


问题引入

        我们的多线程利用在我们的各个场景,最常见的就是在我们的买票的过程中,当多线程没有被我们进行保护,我们可以随便的访问我们的票数,导致出现购买的票号既然是负数,这显然是不可能的,这就是我们多线程中的共享数据的不一致问题,所以我们就需要有一个工具来保护我们的多线程,从而防止下图这种场景发生。

线程代码如下:该例以tickes为全局变量模拟票数。

void *getTicket(void *args)
{
    threadData *td = static_cast<threadData *>(args);
    string name = td->thread_name_;
    while (true)
    {
        if (tickes > 0)
        {
            usleep(10000);
            cout << name << "  get a ticket: " << tickes << endl;
            tickes--;
        }
        else
        {
            break;
        }
    }

    return nullptr;
}

        当我们知道这个现象是由共享数的不一致问题导致的,接下来我们来分析一下,为什么会出现这个现象,知道原因之后,就可以找方法来解决我们的问题。

        在上述代码中,由于tickes是共享数据,假设我们创建四个多线程来模拟四个用户抢票,当我们的tickes>0的时候,就有票可以选,但是我们的多线程是并发的,有可能存在当只有最后一张票的时候,我们的四个线程都执行到了if(tickes>0)这个判断处,当执行usleep时,我们的线程就被切换了(带走了我们的上下文,同时带走了tickes的数值为1),导致我们的四个线程都进入了if语句之内,这就表示着,最后一种票同时有四个用户能够购买。由于我们的tickes--操作并不是原子的(他会被编译成多条汇编语句),他会被分为三步:1、先将tickes的值读入到cpu中,2、CPU内进行运行,3、将运算结果写会内存。当我们的某一个线程已经将我们tickes--操作执行到第三步的时候,我们的tickes在内存中的值就变为了0,当另一个线程(虽然被切换时上下文中的tickes是1,但是输出和其他操作需要重新读取内存)来读取tickes时,就变为了0,然后有进行--操作,导致我们出现上述的结果。

        此时我们就知道了,这是说到底还是因为我们的对共享数据的保护不足,假设我们的多线程,在访问共享资源的时候,永远只有一个执行流进行访问,那是不是就可以保证我们共享数据的安全了呢?是的,我们将这种能够安全访问的共享数据叫做我们的临界资源

接下来我们便来说一说该怎么解决上述的问题,那就是我们的线程锁

Linux线程互斥

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

互斥量mutex

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

需要解决我们上面的这个问题需要做到三点:

  • 代码必须要有互斥行为:当代码进入临界区执行时,不允许其他线程进入该临界区。

  • 如果多个线程同时要求执行临界区的代码,并且临界区没有线程在执行,那么只能允许一个线程进入该临界区。

  • 如果线程不在临界区中执行,那么该线程不能阻止其他线程进入临界区

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

互斥量的接口

初始化互斥量有两种方法:
1.静态分配(全局变量):
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER
2.动态分配:

 #include <pthread.h>
int pthread_mutex_init(pthread_mutex_t *restrict mutex,

                                    const pthread_mutexattr_t *restrict attr);

参数:
mutex :要初始化的互斥量
attr NULL(目前不需要了解设为空即可)
销毁互斥量
  销毁互斥量需要注意:
  • 使用 PTHREAD_ MUTEX_ INITIALIZER 初始化的互斥量不需要销毁 ,也就是全局的不需要销毁
  • 不要销毁一个已经加锁的互斥量
  • 已经销毁的互斥量,要确保后面不会有线程再尝试加锁 

  #include <pthread.h>

 int pthread_mutex_destroy(pthread_mutex_t *mutex);

参数

   mutex:需要销毁的锁的地址

互斥量加锁和解锁

 #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,失败返回错误号

调用 pthread_ lock 时,可能会遇到以下情况 :
  • 互斥量处于未锁状态,该函数会将互斥量锁定,同时返回成功
  • 发起函数调用时,其他线程已经锁定互斥量,或者存在其他线程同时申请互斥量,但没有竞争到互斥量,那么pthread_ lock调用会陷入阻塞(执行流被挂起),等待互斥量解锁,后再去申请锁,知道申请到锁,才可以进入临界区执行,这样就保证了我们当前线程在访问临界区的时候,对于其他的线程来说是原子的。
  •  如果在纯互斥环境中,锁如果分配的不合理,就容易导致某些线程一直无法得到锁,一直在等待,导致出现了线程饥饿问题

        此时我们就思考,既然我们的所以线程都可以访问锁,那我们的锁,是不是共享资源呢?是的!既然我们的共享锁是共享资源,那我们共享锁是怎么做到安全的,也就是原子操作的呢?  

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

解决问题

        当我们已经知道了加锁的方法之后,我们就可以来修改一下我们的代码了,当访问我们的共享资源之前我们就进行加锁处理,当出了临界区之后我们就将锁给释放掉。

修改后的现象如下:此时我们的票数的结果就和我们预期的现象一样了

源码如下:

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

using namespace std;
#define NUM 4

int tickes = 1000;
struct threadData//定义我们的线程数据结构体,存放线程姓名和锁
{
    threadData(int num)
    {
        thread_name_ = "thread-" + to_string(num);
    }

    pthread_mutex_t *lock;
    string thread_name_;
};

void *getTicket(void *args)
{
    threadData *td = static_cast<threadData *>(args);
    string name = td->thread_name_;

 
    while (true)
    {
          pthread_mutex_lock(td->lock);//在访问tickes临界资源之前进行加锁
        if (tickes > 0)
        {
            usleep(10000);
            cout << name << "  get a ticket: " << tickes << endl;
            tickes--;
             pthread_mutex_unlock(td->lock);//访问完成之后,解锁
        }
        else
        {
             pthread_mutex_unlock(td->lock);//如果没有票了也需要解锁
            break;
        }

        usleep(133);
    }

    return nullptr;
}

int main()
{
    vector<pthread_t> tids;
    vector<threadData *> thread_datas;
    pthread_mutex_t lock;
    pthread_mutex_init(&lock, nullptr);

    for (int i = 0; i < NUM; i++)
    {
        pthread_t tid;
        threadData *td = new threadData(i);
        td->lock = &lock;

        thread_datas.push_back(td);
        pthread_create(&tid, nullptr, getTicket, thread_datas[i]);
        tids.push_back(tid);
    }

    for (auto &e : tids)//进行线程等待
    {
        pthread_join(e, nullptr);
    }

    pthread_mutex_destroy(&lock);//摧毁锁
    for (auto &e : thread_datas)
    {
        delete e;
    }

    return 0;
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

晚风吹长发

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值