这里我们简要介绍了线程间互斥相关的概念,并对加锁的一种原理进行了剖析,本人目前理解尚浅,若文中有表述不当的地方还望理解并指正,谢谢大家!
文章目录
- 一:线程间互斥相关背景概念
- 二:互斥量mutex
- 三:互斥量实现原理探究
一:线程间互斥相关背景概念
- 临界资源:多个线程执行流共享的资源叫做临界资源。
- 临界区:每个线程内部,访问临界资源的代码,叫做临界区。
- 互斥:任何时刻,互斥保证有且只有一个执行流进入临界区,访问临界资源,通常对临界资源起保护作用。
- 原子性:不会被任何调度机制打断的操作,该操作只有两态,要么完成了,要么没完成。
二:互斥量mutex
- 大部分情况,线程使用的数据都是局部变量,变量的地址空间在线程栈空间内,这种情况,变量归属单个线程,其他线程无法获取这种变量。
- 但有时候,很多变量都需要在线程间共享,这样的变量称为共享变量,可以通过数据的共享,完成线程之间的交互。
- 多个线程并发的操作共享变量,回应发线程安全问题。
要解决上述因多线程操作共享变量带来的线程安全问题,需要做到以下三点:
- 代码必须要有互斥行为:当代码进入临界区执行时,不允许其他线程进入该临界区。
- 如果多个线程同时要求执行临界区的代码,并且临界区没有线程在执行,那么只允许一个线程进入该临界区。
- 如果线程不在临界区中执行,那么该线程不能阻止其他线程进入临界区。
要做到上述三点,本质就是需要一把锁。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:内存中的一个变量
凡是在寄存器中的数据,全部都是线程的上下文数据,在线程被切换时,会随着线程一起离开。
💡💡💡由上述伪代码,上锁的过程分为三步:
- 将0写入寄存器,mutex值默认为1。
- 交换寄存器和mutex的值。
- 判断寄存器中的内容,如果大于0,则加锁成功,否则线程挂起等待!
如果在判断之前,线程A被切换走了,此时线程A带着寄存器上下文数据(1)一起被切换走,那么此时来的线程因申请不到锁而被挂起等待。
加锁的结果就是把线程访问临界区串行化!