Linux—线程互斥

一、问题引入

首先我们先看如下代码:

int tickets = 10000;
void routine(const string &name)
{
    while (true)
    {
        if (tickets > 0)
        {
            cout << name << "buy a ticket..,tickerts num:" <<tickets<< endl;
            tickets--;
        }
        else
        {
            break;
        }
    }
}

int main()
{
    //thread是我自己封装的一个类
    //创建线程
    thread t1("thread-1", routine);
    thread t2("thread-2", routine);
    thread t3("thread-3", routine);
    thread t4("thread-4", routine);
    //让线程跑起来
    t1.start();
    t2.start();
    t3.start();
    t4.start();
    //等待线程
    t1.join();
    t2.join();
    t3.join();
    t4.join();
}

上述代码是一个多线程购票的代码,由于tickets是定义在主线程的栈空间中的,他可以被所有的线程看到,是一个共享资源,这个代码的初衷是让每个线程参与抢票,当票数为0的时候程序停止,但是我们发现运行结果的票数最后竟然出现了负数,这是怎么回事呢?

【解释】:

假设还剩最后一张票数的时候,线程1要进行if判断首先需要从内存中把tickets的值放到寄存器中,与0进行逻辑运算,线程1判断成功要继续执行代码,但是假设此时线程1时间片到了,轮到线程2执行了,由于线程1没有进行--操作,线程2从内存加载到寄存器的值还是为1,此时线程1和线程2都进入到了购票的代码中,但是票只有一张此时就出问题了,当线程1再次被调度时,会从寄存器中恢复数据,执行--操作,由于- -操作不是原子的,翻译成汇编会有3条指令,1.重读数据 2.--数据3. 写会数据,线程1从内存中重新读取票数为1,执行--操作后,写会内存,此时内存中的票数为0,当线程2在被调度时,从内存中的读取的票数就为0了,--操作后就会出现负数的情况,造成上述情况的根本原因就是因为多线程并发访问共享资源。

要解决以上问题,需要做到三点:

  • 代码必须要有互斥行为:当代码进入临界区执行时,不允许其他线程进入该临 界区。
  • 如果多个线程同时要求执行临界区的代码,并且临界区没有线程在执行,那么 只能允许一个线程进入该临界区。
  • 如果线程不在临界区中执行,那么该线程不能阻止其他线程进入临界区。

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

二、互斥量

2.1进程线程间的互斥相关背景概念

临界资源:多线程执行流共享的资源就叫做临界资源

临界区:每个线程内部,访问临界资源的代码,就叫做临界区,保护临界资源的本质就是保护临界区

互斥:任何时刻,互斥保证有且只有一个执行流进入临界区,访问临界资源, 通常对临界资源起保护作用

原子性(后面讨论如何实现):不会被任何调度机制打断的操作,该操作只有 两态,要么完成,要么未完成

2.2 互斥量接口
初始化互斥量

初始化互斥量有两种方法:

方法 1,静态分配:

//当互斥量定义为全局的时候就可以直接用INITIALIZER初始化
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER

方法 2,动态分配:

//当互斥量定义为局部的时候就需要这个函数初始化了
int pthread_mutex_init(pthread_mutex_t *restrict mutex, const
pthread_mutexattr_t *restrict attr);
参数:
mutex:要初始化的互斥量
attr:NULL
销毁互斥量

销毁互斥量需要注意:

  • 使用 PTHREAD_ MUTEX_ INITIALIZER 初始化的互斥量不需要销毁
  • 不要销毁一个已经加锁的互斥量
  • 已经销毁的互斥量,要确保后面不会有线程再尝试加锁
int pthread_mutex_destroy(pthread_mutex_t *mutex);
互斥量加锁和解锁
int pthread_mutex_lock(pthread_mutex_t *mutex);
int pthread_mutex_unlock(pthread_mutex_t *mutex);
返回值:成功返回 0,失败返回错误号

pthread_mutex_lock函数就是给互斥量加锁,pthread_mutex_unlock是给互斥量解锁,两者中间的代码就是我们需要保护的临界区。当锁申请成功会继续向后执行,当锁申请失败,会阻塞等待,直到其他线程将解锁,重新参与竞争。

接下来我们重新改善一下上面的抢票函数:

//定义并初始化互斥量
pthread_mutex_t mutex=PTHREAD_ADAPTIVE_MUTEX_INITIALIZER_NP;

void routine(const string &name)
{
    while (true)
    {
        pthread_mutex_lock(&mutex);
        if (tickets > 0)
        {
            cout << name << "buy a ticket..,tickerts num:" <<tickets<< endl;
            tickets--;
            pthread_mutex_unlock(&mutex);
        }
        else
        {
            pthread_mutex_unlock(&mutex);
            break;
        }
    }
}

三、互斥量实现原理探究

  • 经过上面的例子,大家已经意识到单纯的 i++或者++i 都不是原子的,有可能 会有数据一致性问题
  • 为了实现互斥锁操作,大多数体系结构都提供了 swap 或 exchange 指令,该指令 的作用是把寄存器和内存单元的数据相交换,由于只有一条指令,保证了原子性,即使 是多处理器平台,访问内存的 总线周期也有先后,一个处理器上的交换指令执行时另 一个处理器的交换指令只能等待总线周期。 现在我们把 lock 和 unlock 的伪代码改 一下
  • 所有的线程申请锁,前提是所有线程可以看到锁,所以锁也是个共享资源,这就要保证加锁解锁的过程是原子的,要么申请上锁了,要么没有,加锁的本质就是将内存中的互斥量置为0,让别的线程申请不到,由于互斥量只有1个,可以保证一个时刻只有一个线程申请到了互斥量。

问题:

加锁和解锁可以保证多线程串行访问临界资源,那在访问期间可不可以有线程切换呢?

【答案】:

这个期间是可以进行切换的,因为线程虽然被切换走了,但是并没有释放锁,其他线程想要访问临界资源依旧申请不到锁,只有线程释放锁以后其他线程才有机会访问临界区

  • 32
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

张呱呱_

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

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

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

打赏作者

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

抵扣说明:

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

余额充值