Linux多线程互斥量与原理剖析

Source

这里我们简要介绍了线程间互斥相关的概念,并对加锁的一种原理进行了剖析,本人目前理解尚浅,若文中有表述不当的地方还望理解并指正,谢谢大家!

一:线程间互斥相关背景概念

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

二:互斥量mutex

  • 大部分情况,线程使用的数据都是局部变量,变量的地址空间在线程栈空间内,这种情况,变量归属单个线程,其他线程无法获取这种变量。
  • 但有时候,很多变量都需要在线程间共享,这样的变量称为共享变量,可以通过数据的共享,完成线程之间的交互。
  • 多个线程并发的操作共享变量,回应发线程安全问题。

要解决上述因多线程操作共享变量带来的线程安全问题,需要做到以下三点

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

要做到上述三点,本质就是需要一把锁。Linux上提供的这把锁叫互斥量。
在这里插入图片描述
互斥量的接口:
(1)初始化互斥量:
方法一:静态分配

pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER

方法二:动态分配

int pthread_mutex_init(pthread_mutex_t* mutex, const pthread_mutexattr_t* restrict attr);
参数:
mutex:要初始化的互斥量
attr:NULL

(2)销毁互斥量
销毁互斥量需要满足:

  • 使用PTHREAD_MUTEX_INITIALIZER初始化的互斥量不需要销毁
  • 不要销毁一个已经加锁的互斥量
  • 已经销毁的互斥量,要确保后面不会再有线程尝试加锁。

int pthread_mutex_destroy(pthread_mutex_t* mutex);

(3)互斥量加锁和解锁

int pthread_mutex_lock(pthread_mutex_t* mutex);
int pthread_mutex_unlock(pthread_mutex_t* mutex);
返回值:成功返回0,失败返回错误号

调用pthread_mutex_lock时,可能遇到以下情况:

  • 互斥量处于未锁状态,该函数会将互斥量锁定,同时返回成功。
  • 发起函数调用时,其他线程已经锁定互斥量,或者存在其他线程同时申请互斥量,但没有竞争到互斥量,那么pthread_mutex_lock调用会陷入阻塞(执行流被挂起),等待互斥量解锁。

三:互斥量实现原理探究

  • 在多线程中,不难意识到单纯的i++或者++i都不是原子操作,原因是经过汇编后由多条语句构成,又因为线程在任何时刻都可能被切换,所以有可能会导致数据不一致问题。
  • 为了实现互斥锁操作,大多数体系结构都提供了swap或exchange指令,该指令的作用是把寄存器和内存单元的数据做交换;由于只有一条语句,所以保证了原子性

线程加锁是如何保证原子性的?下面我们看lock的一段伪代码:
在这里插入图片描述
%al:寄存器;
mutex:内存中的一个变量
凡是在寄存器中的数据,全部都是线程的上下文数据,在线程被切换时,会随着线程一起离开。

💡💡💡由上述伪代码,上锁的过程分为三步:

  1. 将0写入寄存器,mutex值默认为1。
  2. 交换寄存器和mutex的值。
  3. 判断寄存器中的内容,如果大于0,则加锁成功,否则线程挂起等待!
    在这里插入图片描述
    在这里插入图片描述

如果在判断之前,线程A被切换走了,此时线程A带着寄存器上下文数据(1)一起被切换走,那么此时来的线程因申请不到锁而被挂起等待。

加锁的结果就是把线程访问临界区串行化!