Linux - 线程互斥和互斥锁


前言

前几节课,我们学习了多线程的基础概念,这节课,我们来对线程互斥和互斥锁的内容进行学习。


一、为什么要线程互斥

首先我们要明白,对于多线程,其实就是多个执行流在同时执行各自的代码。 而有的时候,我们多个执行流可能会同时访问到同一份资源,我们称这种资源叫做临界资源,而我们各个执行流访问这些临界资源的代码,就叫做临界区

当我们多个执行流访问临界资源时,就可能由于OS的线程时间调度问题,导致临界资源出现紊乱问题,所以,对于这种情况,我们就需要线程互斥来保护临界资源。

示例代码

#include <iostream>
#include <unistd.h>
#include <cstdio>
#include <pthread.h>
#include <string>
#define TNUM 5

int ticket = 10000;

void *grab(void *args)
{
    const std::string name = (char *)args;

    while (1)
    {
    	// --- 临界区
        if (ticket > 0)
        {
            usleep(1000);
            printf("%s : sells ticket:%d\n", name.c_str(), ticket);
            ticket--;
        // --- 临界区
        }
        else
        {
            break;
        }
    }
    return nullptr;
}

int main()
{
    pthread_t tid[TNUM];

    for (int i = 0; i < TNUM; i++)
    {
        char name[64];
        snprintf(name, sizeof name, "new thread %d", i + 1);
        pthread_create(tid + i, nullptr, grab, (void *)name);
        usleep(10000);
    }

    for (int i = 0; i < TNUM; i++)
    {
        pthread_join(tid[i], nullptr);
    }
    return 0;
}

如同此代码,这是一个抢票系统的简易代码,五个线程都执行抢票代码,对全局变量ticket进行–操作。

这串代码看上去似乎没有问题,但是在多线程的情况下,就可能会导致问题。
在这里插入图片描述
可是为什么呢? 我们的if判断不是如果ticket<0就break吗?

这是因为,我们的–操作在汇编角度,其实是三条语句。

第一步是将内存中的ticket move 到 寄存器中
第二步才是进行计算操作
第三步是将新计算的ticket move 到内存中

而操作系统在进行线程调度的时候,可不是管你是进行到第一步,可能你才刚执行完第一步,你的时间片就到了,然后你就被操作系统丢到运行队列末尾了,这就可能会导致临界资源不正常。

原子性

解答这个问题,就需要提出一个概念。叫做原子性。
原子性就是 我们要么不做,要么就把这件事做完,而通常而言,我们可以理解为一条汇编就是原子性的。

二、互斥锁

对于多线程库pthread,也必然会考虑到上面这种问题,所以就设计了互斥锁来保护我们的临界资源!

man 3 pthread_mutex_destroy
man 3 pthread_lock
man 3 pthread_unlock

互斥锁的创建与销毁

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

pthread_mutex_t 就是pthread库给我们提供的锁的数据结构。

参数pthread_mutex_t* restrict_mutex 是我们需要初始化的锁。
参数const pthread_mutexattr_t *restrict attr 我们这里不做考虑,设为nullptr即可。

需要注意的是 对于mutex互斥锁的创建和初始化有两种。

第一种是对于静态或全局的pthread_mutex_t ,我们可以使用 pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER; 这种方式直接进行初始化,而不需要调用pthread_mutex_init函数,也不需要再调用 pthread_mutex_destroy进行销毁

第二种是对于局部的pthread_mutex_t ,我们就必须要调用 pthread_mutex_init来进行初始化,最后再调用 pthread_mutex_destroy进行销毁。

互斥锁进行互斥

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

将mutex 理解为一把锁,且这把锁只有一把钥匙

pthread_mutex_lock函数是申请锁的钥匙,申请到了锁的钥匙则可以继续向下执行,如果没有申请到,则挂起等待。

pthread_mutex_unlock函数就是归还钥匙。

在这里插入图片描述
这是lock的伪代码,意思就是将0放入到寄存器%al中,然后%al寄存器中的数据与内存中的mutex数据交换,本质就是共享<->私有的过程,将唯一锁变成私有的。

需要注意的是,在汇编中exchan是一条汇编,代表这是原子性的,也就保证了锁的安全性。
在这里插入图片描述
这是unlock的伪代码,将1存入到内存中的mutex中。

#include <iostream>
#include <unistd.h>
#include <cstdio>
#include <pthread.h>
#include <string>
#define TNUM 5

int ticket = 10000;

pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;

void *grab(void *args)
{
    const std::string name = (char *)args;

    while (1)
    {
        pthread_mutex_lock(&mutex);
        // --- 临界区
        if (ticket > 0)
        {
            usleep(1000);

            printf("%s : sells ticket:%d\n", name.c_str(), ticket);
            ticket--;
        // --- 临界区
        pthread_mutex_unlock(&mutex);
        }
        else
        {
            pthread_mutex_unlock(&mutex);
            break;
        }
        usleep(1000);//抢完票的后续动作
    }
    return nullptr;
}

int main()
{
    pthread_t tid[TNUM];

    for (int i = 0; i < TNUM; i++)
    {
        char name[64];
        snprintf(name, sizeof name, "new thread %d", i + 1);
        pthread_create(tid + i, nullptr, grab, (void *)name);
        usleep(10000);
    }

    for (int i = 0; i < TNUM; i++)
    {
        pthread_join(tid[i], nullptr);
    }
    return 0;
}

在这里插入图片描述

加入了互斥锁之后,结果就没有问题了。


  • 17
    点赞
  • 26
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

风君子吖

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

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

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

打赏作者

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

抵扣说明:

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

余额充值