【Linux】—— Linux线程互斥

Linux线程互斥

前面我们在 Linux线程基本概念 介绍了线程基本概念,在Linux线程控制中介绍了线程创建,线程终止,线程等待,线程分离等等概念,今天我们来介绍一下线程互斥的相关概念

线程并发带来的问题

互斥概念

我们之前在 进程间通信之匿名管道 讲管道相关概念时我们提到了一些与进程线程间互斥相关的背景概念,我们今天站在线程的角度来回忆一下

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

并发问题

  • 并发在操作系统中,是指一个时间段中有几个程序都处于已启动运行到运行完毕之间,且这几个程序都是在同一个处理机上运行,但任一个时刻点上只有一个程序在处理机上运行。
  • 并发可能存在某一时刻会对共享资源进行同时访问,如果在对这些共享资源不遵循规则,那么就可能对这些共享资源造成破坏。接下来我们使用一个最简单的代码来阐述并发出现的问题。
    【Linux】—— Linux线程互斥_第1张图片
  • 上述问题我们也清晰的看到了,那为什么会引发这样的问题呢,其实最主要的原因还是对全局变量total++这一个操作并不是原子性的,它分为三个个汇编指令,load:将共享变量从内存加载到寄存器,update:对变量进行+1操作,store:再将共享变量由回寄存器写回内存
    【Linux】—— Linux线程互斥_第2张图片
  • 那我们来想象一下线程在进行操作的时候,这里有多个线程等着给total++,在一个线程将该变量从内存加载寄存器中之后,还在进行++操作,这时又来了一个线程将原来未++的变量取出进行++,之后分别写回内存中,这样其实这两个线程其实只对该变量++了一次

互斥量mutex

我们要解决上述问题,需要做到以下三点:

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

要做到这三点,本质上就是需要一把锁,linux上提供了这把锁叫互斥量metux
【Linux】—— Linux线程互斥_第3张图片

互斥量接口

初始化互斥量
  • 静态分配
    初始化互斥量
  • 动态分配
    【Linux】—— Linux线程互斥_第4张图片
销毁互斥量

销毁互斥量需要注意:

  • 使用PTHREAD_ MUTEX_ INITIALIZER初始化的互斥量不需要销毁
  • 不要销毁一个已经加锁的互斥量
  • 已经销毁的互斥量,要确保之后不会有任何线程再尝试加锁

【Linux】—— Linux线程互斥_第5张图片

互斥量的加锁和解锁

【Linux】—— Linux线程互斥_第6张图片
使用pthread_mutex_lock时可能会出现以下情况:

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

我们将刚刚的代码修改一下,给它加上互斥锁
【Linux】—— Linux线程互斥_第7张图片

互斥锁的原理

互斥锁的实现原理实际上非常的简单,他并没有你想的那么复杂,这里我们必须要知道的前提是我们可以将汇编的一句指令认为是原子的,所以为了实现锁的互斥操作,大多数体系结构对于互斥锁的实现使用了swap或者exchange指令,该指令的作用是把寄存器和单元数据交换,该指令只有一句,所以是原子操作,因为就算是多线程,总线周期也有先后,所以利用此指令总能保证同一个时间只有一个线程进入临界区。

现在我们来看看如何使用swap或者exchange指令实现互斥锁原理,来看下面的一段伪代码:可以看到锁资源最开始拿到的值为1,然后使用swap或exchange指令将寄存器中的值和?拥有的值交换,如果寄存器中现在值为1,那么表示资源申请成功,如果寄存器中值为0,说明?资源中的1已经被其他线程交换走了,所以当前进程也就需要进入等待队列

lock:
		mov $0, %al
		xchgb %al, mutex
		if (al寄存器的内容 > 0){
			return 0;
		}
		else{
			挂起等待;
		}
		goto lock;
unlock:
		movb $1, mutex
		唤醒等待mutex的线程
		return 0;

重入

我们上面加加变量的小栗子有问题其实另一个原因是他本质是一个不可重入的函数,什么是重入?重入是指同一个函数被不同的执行流调用,当前一个流程还没有执行完,就有其他的执行流再次进入,我们称之为重入。一个函数在重入的情况下不会出现不同或者任何问题,则我们称其为可重入函数,否则反之称为不可重入函数。

常见不可重入的情况:

  • 调用了malloc和free函数,因为malloc是使用全局链表来管理堆的
  • 调用了标准I/O库函数,I/O库很多函数的实现都使用了全局的数据结构
  • 函数体使用了静态全局变量

常见可重入的情况:

  • 不使用全局变量和静态变量
  • 不适用malloc或者new开辟出的空间
  • 不调用不可重入函数

线程安全问题

线程安全是指多个线程并发执行一段代码时,不会出现不同的结果。常见对全局变量或者静态变量进行操作,如果没有互斥锁的保护的情况下,会出现该问题。

常见线程不安全的情况

  • 不保护共享变量的函数
  • 函数的状态随着被调用,函数发生变化的函数
  • 返回指向静态变量指针的函数
  • 调用线程不安全函数的函数

常见线程安全的情况

  • 每个线程对于全局变量或者静态变量只有读取的权限,而没有写入的权限
  • 类或者接口对于线程来说都是原子操作
  • 多个线程之间的切换不会导致该接口的执行结果存在二义性

线程安全 VS 重入

  • 很经典的一句话,可重入函数一定是线程安全的,但是线程安全的不一定是可重入函数
  • 函数是不可重入的,那么就不能由多个线程使用,有可能引发线程安全的问题
  • 如果对临界资源访问加上锁,则这个函数线程安全,但是如果这个重入函数若锁没有释放则会产生死锁问题

你可能感兴趣的:(Linux,线程互斥,互斥量接口,互斥量原理,常见线程安全问题)