【Linux】线程的互斥

因为线程是共享地址空间的,就会共享大部分资源,这种共享资源就是公共资源,当多执行流访问公共资源的时候,就会出现各种情况的数据不一致问题。为了解决这种问题,我们就需要学习线程的同步与互斥,本篇将介绍线程的互斥。

1.相关概念

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

1.1 数据不一致

⼤部分情况,线程使⽤的数据都是局部变量,变量的地址空间在线程栈空间内,这种情况,变量
归属单个线程,其他线程⽆法获得这种变量。 但有时候,很多变量都需要在线程间共享,这样的变量称为共享变量,可以通过数据的共享,完成线程之间的交互。

下面是一个模拟抢票的简单代码。

cpp 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <pthread.h>

int ticket = 100;
void *route(void *arg)
{
    char *id = (char *)arg;
    while (1)
    {
        if (ticket > 0) // 判断票的数量
        {
            usleep(1000);                               // 模拟抢票的时候花费的时间
            printf("%s sells ticket:%d\n", id, ticket); // 假设这里就是抢到票了
            ticket--;                                   // 更新票的数量
        }
        else
        {
            break;
        }
    }
    return nullptr;
}

int main()
{
    pthread_t t1, t2, t3, t4;
    pthread_create(&t1, NULL, route, (void *)"thread 1");
    pthread_create(&t2, NULL, route, (void *)"thread 2");
    pthread_create(&t3, NULL, route, (void *)"thread 3");
    pthread_create(&t4, NULL, route, (void *)"thread 4");

    pthread_join(t1, NULL);
    pthread_join(t2, NULL);
    pthread_join(t3, NULL);
    pthread_join(t4, NULL);
    return 0;
}

运行后会发现,这个票数居然还减到了负数。

主要是因为usleep让所有线程在判断tickets>0时,全部进到判断里,但是usleep却不让线程往后执行,大大提升了线程被同时进到临界区的机会,tickets就会被减到负数。(不一致原因详情:课42)

全局资源没有加保护就可能会有并发问题,这也是线程安全问题。

1.2 见一见锁

解决上面出现的问题我们可以给临界区代码加锁。

mutex互斥锁,也叫互斥量,它的类型就叫pthread_mutex_t,使用锁需要头文件pthread.h

使用的时候先对锁初始化,直接用PTHREAD_MUTEX_INITIALIZER这个宏初始化就行。

cpp 复制代码
int ticket = 100;
pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER; // 定义一把锁并初始化
void *route(void *arg)
{
    char *id = (char *)arg;
    while (1)
    {
        pthread_mutex_lock(&lock); // 加锁
        if (ticket > 0) // 判断票的数量
        {
            usleep(1000);                               // 模拟抢票的时候花费的时间
            printf("%s sells ticket:%d\n", id, ticket); // 假设这里就是抢到票了
            ticket--;                                   // 更新票的数量
            pthread_mutex_unlock(&lock); // 解锁
        }
        else
        {
            pthread_mutex_unlock(&lock); // 防止走到else时锁没解
            break;
        }
    }
    return nullptr;
}

可以看到加锁之后就没有出现数据被减到负数了,而且还能感受到这个代码的运行速度变慢了。

2.认识mutex

  • 全局的锁:这种方式定义的锁不用被释放,程序运行结束会自动释放。
  • 局部的锁:就要用到相关的函数,初始化锁的函数第二个参数就是锁的一些属性,不用管。局部的锁要调用destroy释放。
  • 不管是全局的还是局部的锁,线程在访问公共资源之前都要申请锁,lock加锁,unlock解锁。线程申请锁成功,继续向后运行,申请失败会阻塞挂起申请执行流。trylock是非阻塞版本,不考虑。

所有线程都要竞争申请锁,所以首先所有线程都要看到锁,所以锁本身就是临界资源;锁是用来保护临界区资源的,但是谁来保护锁?所以要求锁的申请和解除必须是原子的

锁提供的能力本质就是:执行临界区代码的执行流由并行转为串行

2.1 接口使用

前面我们已经使用过全局的锁了,现在就用一下局部锁。

cpp 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <pthread.h>
#include <string>
#include <iostream>

int ticket = 100;
struct Data
{
    Data(const std::string &name, pthread_mutex_t *plock)
        : _name(name),
          _plock(plock)
    {}

    std::string _name;
    pthread_mutex_t *_plock;
};

void *route(void *arg)
{
    Data* d = static_cast<Data *>(arg);
    while (1)
    {
        pthread_mutex_lock(d->_plock); // 加锁
        if (ticket > 0)
        {
            usleep(1000);
            printf("%s sells ticket:%d\n", d->_name.c_str(), ticket);
            ticket--;
            pthread_mutex_unlock(d->_plock); // 解锁
        }
        else
        {
            pthread_mutex_unlock(d->_plock); // 防止走到else时锁没解
            break;
        }
    }
    return nullptr;
}

int main()
{
    pthread_mutex_t lock;  // 局部锁
    pthread_mutex_init(&lock, nullptr); // 对锁初始化

    Data d1("thread 1", &lock);
    Data d2("thread 2", &lock);
    Data d3("thread 3", &lock);
    Data d4("thread 4", &lock);

    pthread_t t1, t2, t3, t4;
    pthread_create(&t1, NULL, route, &d1);
    pthread_create(&t2, NULL, route, &d2);
    pthread_create(&t3, NULL, route, &d3);
    pthread_create(&t4, NULL, route, &d4);

    pthread_join(t1, NULL);
    pthread_join(t2, NULL);
    pthread_join(t3, NULL);
    pthread_join(t4, NULL);

    pthread_mutex_destroy(&lock); // 销毁锁

    return 0;
}

操作还是比较简单的。

对临界区资源进行保护,本质就是用锁对临界区代码进行保护。

  • 加锁之后,在临界区内部,依旧允许线程切换,因为当前线程并没有释放锁,依旧持有锁,带着锁被切换的,其他的线程必须等我回来执行完代码,将锁释放后,他们才可以展开对锁的竞争从而进入临界区。
  • 这把锁要么没被使用要么已经被使用完了,这两种状态才对其他线程有意义,这就体现了原子性。
  • 在线程访问临界区资源时不会被其他线程打扰,也是一种变相的原子性的表现。

2.2 mutex的原理

硬件实现:关闭时钟中断(了解即可)。

软件实现:为了实现互斥锁操作,⼤多数体系结构都提供了swapexchange汇编指令,该指令的作⽤是把寄存器和内存单元的数据相交换,由于只有⼀条指令,保证了原⼦性,下面有段伪代码。

申请锁

进程/线程切换:CPU内部的寄存器硬件只有一套,但是CPU寄存器的数据可以有多份,每份就是当前执行流的上下文数据。

把一个变量的内容换到CPU内部,其实就是把变量的内容获取到当前执行流的硬件上下文中,CPU寄存器的硬件上下文属于进程/线程私有的。

我们用swapexchange将内存中的变量交换到寄存器中,其实就是当前进程/线程在获取锁,是交换,而不是拷贝,所以锁只有一份,谁申请谁持有。

当后面来的执行流想申请锁,首先会把寄存器清0,然后在交换的这一步时,就只会用0换0,因为这个1已经被之前的线程申请走了,此时申请锁失败,线程就会阻塞挂起。

解锁

解锁的时候,只需要往内存里的mutex写1

2.3 C++里的mutex

cpp 复制代码
#include <mutex> //需要包含的头文件

std::mutex cpp_mutex;  //定义锁

cpp_mutex.lock(); //加锁
cpp_mutex.unlock(); //解锁

3.封装mutex

cpp 复制代码
//Mutex.hpp文件
#include <iostream>
#include <pthread.h>
#include <cstring>
#include <cstdio>

namespace MyMutex
{
    class Mutex
    {
    public:
        Mutex()
        {
            pthread_mutex_init(_plock, nullptr); // 锁初始化
        }
        void Lock() // 加锁
        {
            int n = pthread_mutex_lock(_plock);
            if (n != 0)
                std::cerr << "pthread_mutex_lock fail: " << strerror(n) << std::endl;
        }
        void UnLock() // 解锁
        {
            int n = pthread_mutex_unlock(_plock);
            if (n != 0)
                std::cerr << "pthread_mutex_unlock fail: " << strerror(n) << std::endl;
        }

        ~Mutex()
        {
            pthread_mutex_destroy(_plock); // 锁释放
        }

    private:
        pthread_mutex_t *_plock;
    };
}
cpp 复制代码
//测试
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <string>
#include <iostream>
#include "Mutex.hpp"

using namespace MyMutex;

int ticket = 100;
struct Data
{
    Data(const std::string &name, Mutex *plock)
        : _name(name),
          _plock(plock)
    {
    }

    std::string _name;
    Mutex *_plock;
};

void *route(void *arg)
{
    Data *d = static_cast<Data *>(arg);
    while (1)
    {
        d->_plock->Lock(); // 加锁
        if (ticket > 0)
        {
            usleep(1000);
            printf("%s sells ticket:%d\n", d->_name.c_str(), ticket);
            ticket--;
            d->_plock->UnLock(); // 解锁
        }
        else
        {
            d->_plock->UnLock(); // 解锁
            break;
        }
    }
    return nullptr;
}

int main()
{
    Mutex lock; //用自己实现的锁
    Data d1("thread 1", &lock);
    Data d2("thread 2", &lock);
    Data d3("thread 3", &lock);
    Data d4("thread 4", &lock);

    pthread_t t1, t2, t3, t4;
    pthread_create(&t1, NULL, route, &d1);
    pthread_create(&t2, NULL, route, &d2);
    pthread_create(&t3, NULL, route, &d3);
    pthread_create(&t4, NULL, route, &d4);

    pthread_join(t1, NULL);
    pthread_join(t2, NULL);
    pthread_join(t3, NULL);
    pthread_join(t4, NULL);

    return 0;
}

我们还可以进一步封装这个锁,让他可以自动的加锁解锁。需要在实现一个LockGuard类。

cpp 复制代码
#include <iostream>
#include <pthread.h>
#include <cstring>
#include <cstdio>

namespace MyMutex
{
    class Mutex
    {
    public:
        Mutex()
        {
            pthread_mutex_init(_plock, nullptr); // 锁初始化
        }
        void Lock() // 加锁
        {
            int n = pthread_mutex_lock(_plock);
            if (n != 0)
                std::cerr << "pthread_mutex_lock fail: " << strerror(n) << std::endl;
        }
        void UnLock() // 解锁
        {
            int n = pthread_mutex_unlock(_plock);
            if (n != 0)
                std::cerr << "pthread_mutex_unlock fail: " << strerror(n) << std::endl;
        }

        ~Mutex()
        {
            pthread_mutex_destroy(_plock); // 锁释放
        }

    private:
        pthread_mutex_t *_plock;
    };

    class LockGuard
    {
    public:
        LockGuard(Mutex *mutex)
            : _mutex(mutex)
        {
            _mutex->Lock(); // 构造时加锁
        }

        ~LockGuard()
        {
            _mutex->UnLock(); // 析构时解锁
        }

    private:
        Mutex *_mutex;
    };
}
cpp 复制代码
void *route(void *arg)
{
    Data *d = static_cast<Data *>(arg);
    while (1)
    {
        {
            LockGuard lock_guard(d->_plock);
            if (ticket > 0)
            {
                usleep(1000);
                printf("%s sells ticket:%d\n", d->_name.c_str(), ticket);
                ticket--;
            }
            else
            {
                break;
            }
        }
    }
    return nullptr;
}

这个就叫做RAII风格的互斥锁实现。

本篇分享就到这里,我们下篇见~

相关推荐
mxd018482 小时前
最常用的js加解密之RSA-SHA256 加密算法简介与 jsjiami 的结合使用指南
开发语言·javascript·ecmascript
学编程的小鬼2 小时前
SpringBoot日志
java·后端·springboot
gopyer2 小时前
180课时吃透Go语言游戏后端开发7:Go语言中的函数
开发语言·游戏·golang·go·函数
来不及辣哎呀2 小时前
学习Java第三十天——黑马点评37~42
java·开发语言·学习
半桶水专家2 小时前
C语言中的setitimer函数详解
c语言·开发语言·算法
失散132 小时前
分布式专题——26 BIO、NIO编程与直接内存、零拷贝深入辨析
java·分布式·rpc·架构·nio·零拷贝
zhangfeng11333 小时前
R语言 安装老一点的班装包 核心是从CRAN归档(Archive)下载对应版本的安装包
开发语言·r语言
lbwxxc3 小时前
go 基础
开发语言·后端·golang