深入了解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了。

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

相关推荐
A小辣椒8 小时前
TShark:Wireshark CLI 功能
linux
A小辣椒12 小时前
TShark:基础知识
linux
AlfredZhao14 小时前
OCI 明明分配了 200G 系统盘,为什么 df 只看到 30G?
linux·oci
AlfredZhao1 天前
vi 删除指定范围的行,不用再反复按 dd
linux·vi
用户9718356334661 天前
银河麒麟 KY10 申威(SW64) 安装 nginx-1.16.1-2.p01.ky10.sw_64.rpm 详细步骤
linux
猪脚踏浪1 天前
linux 拷贝文件或目录到指定的位置
linux
大树882 天前
金刚石散热越强,管路越先见顶
大数据·运维·服务器·人工智能·ai
摇滚侠2 天前
Linux CentOS7 rpm 安装 MySQL 5.7
linux·运维·mysql
霸道流氓气质2 天前
领域驱动设计(DDD)在 Spring Boot 微服务中的实践指南
运维·spring boot·微服务
bush42 天前
嵌入式linux学习记录十四、术语
linux·嵌入式