深入了解linux系统—— 线程互斥

互斥

在之前的学习中,了解到了临界资源、临界区、等等。

临界资源:多线程(执行流)共享的资源。

临界区:每一个线程(执行流)内部访问临界资源的代码。

**互斥:简单来说就是在任何时刻保证有且只有一个执行流进入临界区,访问临界资源;**对临界资源起保护作用

原子性:不会被任何调度机制打断的操作;要么完成,要么未完成

互斥量mutex

在了解互斥量之前,先来看以下代码:

cpp 复制代码
#include <iostream>
#include <unistd.h>
int count = 1000; 
void *ticket(void *args)
{
    // 预定演唱会门票
    std::string name = static_cast<char *>(args);
    while (count > 0)
    {
        std::cout << name << " 售出门票 : " << count << std::endl;
        count--;
        usleep(100);
    }
    return (void *)100;
}
int main()
{
    pthread_t t1, t2, t3, t4;
    pthread_create(&t1, nullptr, ticket, (void *)"pthread-1 ");
    pthread_create(&t2, nullptr, ticket, (void *)"pthread-2 ");
    pthread_create(&t3, nullptr, ticket, (void *)"pthread-3 ");
    pthread_create(&t4, nullptr, ticket, (void *)"pthread-4 ");
    pthread_join(t1, nullptr);
    pthread_join(t2, nullptr);
    pthread_join(t3, nullptr);
    pthread_join(t4, nullptr);
    return 0;
}

上述代码,模拟演唱会门票的预定,由4个线程(执行流)同时进行售票;

看一下运行结果:

可以发现,一共1000张门票,4个线程(执行流)同时进行售票(count--);

在售票的过程中,竟然出现了-1-2

为什么会出现这种情况呢?

很简单,多个执行流访问count,当一个线程执行到count--时,另一个线程已经进入whlile循环,这样count--后已经<0了;

此外,如果线程执行while内代码,还未执行到count--,线程就被切换了;此时其他线程执行count还是满足条件的。

那也就是说,线程在访问count时,会被别的线程打扰,从而导致数据不一致问题。

原子性

此外,对于上述代码还存在一个问题,那就是count--不是原子的。

到这里,可能会感觉很懵,count--不是原子的?

复制代码
objdump -d a.out > test.objdump
152 40064b: 8b 05 e3 04 20 00 mov 0x2004e3(%rip),%eax # 
600b34 <count>
153 400651: 83 e8 01 sub $0x1,%eax
154 400654: 89 05 da 04 20 00 mov %eax,0x2004da(%rip) # 
600b34 <count>

通过反汇编,可以看一下count--部分的汇编代码,可以看到是分三步进行的:

  • load:将共享变量count从内存加载到寄存器中
  • update:更新寄存器中的值,执行-1操作
  • store:将新值从寄存器中写回共享变量count的内存地址

互斥锁mutex

对于上述的多执行流访问临界资源,导致数据不一致问题;

所以必须有互斥行为:

代码进入临界区执行时,不允许其他线程进入临界区;

简单来说就是:在任意时刻只允许一个线程(执行流)访问临界资源

要做到在任意时刻只有一个执行流访问资源,那就需要一把锁(互斥锁)

对临界区加锁,在执行临界区代码之前,先申请锁。

  • 如果申请成功,说明当前没有线程访问临界资源,可以继续执行;
  • 申请失败则说明当前有线程正在访问临界资源(执行临界区代码),就要等待。

当执行完临界区代码,就要释放锁,让其他线程可以进行临界区访问临界资源。

Linux上提供的这把锁叫互斥量。

1. 创建(初始化)信号量

创建并初始化信号量有两种方式:动态分配/静态分配;

简单来说就是定义全局变量/局部变量

静态分配:

c 复制代码
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;

动态分配:

如果将互斥量定义局部变量,就需要我们手动调用相关接口去初始化和销毁该信号量。的点点滴滴的点点滴滴

初始化互斥量接口函数:pthread_mutex_init

c 复制代码
int pthread_mutex_init(pthread_mutex_t *restrict mutex, const pthread_mutexattr_t *restrict attr);

参数:

参数一:指要初始化的互斥量。

参数二:可以设置互斥量的相关属性,nullptr表示默认。

返回值:

初始化成功返回0,失败返回对应的错误码(非0)

2. 销毁互斥量

创建出来的互斥量需要进行销毁:

复制代码
int pthread_mutex_destroy(pthread_mutex_t *mutex);

参数和返回值就简单多了:传递要销毁的互斥量;

销毁成功返回0,失败则返回对应错误码(非0

在调用pthread_mutex_destroy时要注意:

  1. 使用PTHREAD_MUTEX_INITIALIZER初始化的互斥量不能销毁
  2. 不能销毁一个已经加锁的互斥量
  3. 对于要销毁的互斥量,要保证后面不会再被使用

3. 加锁和解锁

c 复制代码
int pthread_mutex_lock(pthread_mutex_t *mutex);
int pthread_mutex_unlock(pthread_mutex_t *mutex);

调用pthread_mutex_lock,如果互斥量未被锁定(未锁)该函数将互斥量锁定,然后返回;

如果互斥量处于锁定状态,(或者存在其他线程同时申请互斥量,但是没有竞争到互斥量),那pthread_lock就会陷入阻塞(执行流被挂起)等待互斥量解锁再继续运行。

简单来说就时,调用pthread_mutex_lock申请互斥量,申请成功就返回继续运行;申请失败就阻塞等待。

调用pthread_mutex_unlock,对互斥量解锁。

所以,有了互斥量;我们就可以随上面模拟售票的代码进行加锁;

保证在任意时刻,最多只有一个线程进入临界区访问临界资源:

cpp 复制代码
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
int count = 1000;
void *ticket(void *args)
{
    // 预定演唱会门票
    std::string name = static_cast<char *>(args);
    while (count > 0)
    {
        pthread_mutex_lock(&mutex);
        if (count > 0)
        {
            std::cout << name << " 售出门票 : " << count << std::endl;

            count--;
        }
        pthread_mutex_unlock(&mutex);
        usleep(100);
    }
    return (void *)100;
}

这里使用PTHREAD_MUTEX_INITIALIZER,我们也可以创建局部互斥量,然后调用pthread_mutex_initphtread_mutex_destroy初始化和销毁互斥量。

创建局部互斥量,这里要让线程访问到同一个互斥量,可以使用全局指针、也可以通过pthread_create创建线程时的参数传递给线程。

互斥量实现原理

我们知道,对于count++它并不是原子的;那互斥量呢?申请互斥量和释放互斥量是原子的吗?是的

为了实现互斥量,绝大部分体系结构都提供了swap或者esxchange指令,该指令可以把寄存器和内存单元的数据交换;因为只有一条指令,就保证了原子性。

那互斥量是如何实现的呢?

简单来说就是:在某一个寄存器中,存储值1;当线程调用pthread_mutex_lock时,就会交换当前线程互斥量的值和寄存器中的值。

当线程调用pthread_mutex_lock拿到寄存器中的值1,就表示该线程申请锁成功;其他线程再去申请时,寄存器存储的值为0就申请锁失败,就会被挂起等待。

当调用pthread_mutex_unlock时,就会对寄存器写入1;表示释放该互斥量。

互斥量封装

对于互斥量,C++中也存在对应的互斥量类;

这里简单对互斥量进行封装:

cpp 复制代码
class mutex
{
    mutex()
    {
        pthread_mutex_init(&_mutex, nullptr);
    }
    ~mutex()
    {
        pthread_mutex_destroy(&_mutex);
    }
    void Lock()
    {
        pthread_mutex_lock(&_mutex);
    }
    void Unlock()
    {
        pthread_mutex_unlock(&_mutex);
    }

private:
    pthread_mutex_t _mutex;
};

这样在使用时,就可以面向对象式的调用LockUnlock来进行申请锁和释放锁了

此外,我们还可以随mutex再次封装实现自动申请和释放锁。

cpp 复制代码
class lockgroup
{
public:
    lockgroup()
    {
        _mutex.Lock();
    }
    ~lockgroup()
    {
        _mutex.Unlock();
    }

private:
    mutex _mutex;
};

这样在申请和释放锁时,就不需要显式调用LockUnlock了。

到这里本篇文章内容就结束了,感谢支持

相关推荐
奇妙-10 分钟前
创龙3576ububuntu系统设置静态IP方法
linux
末央&40 分钟前
【JavaEE】文件IO操作
java·服务器·java-ee
Jayyih1 小时前
嵌入式系统学习Day23(进程)
linux·运维·服务器
森之鸟1 小时前
审核问题——鸿蒙审核返回安装失败,可以尝试云调试
服务器·前端·数据库
Johny_Zhao1 小时前
Conda、Anaconda、Miniconda对比分析
linux·网络安全·信息安全·kubernetes·云计算·conda·shell·containerd·anaconda·yum源·系统运维·miniconda
大数据小墨1 小时前
在Arch Linux上设置SDDM自动登录Hyprland
linux
shylyly_2 小时前
Linux->多线程3
java·linux·开发语言·阻塞队列·生产者消费者模型
小王努力学编程2 小时前
从零开始的 Docker 之旅
linux·运维·服务器·docker·容器·容器编排·镜像制作
神秘人X7072 小时前
Ansible自动化运维介绍与安装
运维·自动化·ansible
望获linux2 小时前
【实时Linux实战系列】基于实时Linux的音频实时监控系统
大数据·linux·服务器·网络·数据库·操作系统·嵌入式软件