上一篇博客中我们介绍了Linux下对线程控制的简单操作,其实就光单单论线程控制函数的使用来说控制线程并不是一件很难的事,但是线程控制真正抛给我们的难题恐怕并不是仅仅会使用函数而已,本篇博客就带领大家详细的探讨一下Linux下的线程互斥问题。
并发在操作系统中,是指一个时间段中有几个程序都处于已启动运行到运行完毕之间,且这几个程序都是在同一个处理机上运行,但任一个时刻点上只有一个程序在处理机上运行。戳这个搞笑视频或许能帮你加深理解
所以并发可能存在某一时刻会对共享资源进行同时访问,如果在对这些共享资源不遵循规则,那么就可能对这些共享资源造成破坏。多说无益,我们使用一个最简单的代码来阐述并发出现的问题。
结果你认为是20000000,但是实际上令人大跌眼镜,这如果是钞票的话,笔者已经哭死在厕所
那么造成上面结果的原因是什么呢?来看下图,因为程序是并发运行的,所以不同的线程在执行时可能不同的寄存器同时取走了某一时刻的goal,对他们分别进行++后结果都变成了2,最后写回内存结果从预期的三变成了2,这里的根本原因是++这个操作并不是原子的,有的同学一脸震惊,这里不明摆着是一句代码吗,怎么就不是原子的了
来看我们在vs2013下一段简单的反汇编代码:从图中你清楚的可以看到对于goal的++操作分解成了三步,所以这就是导致上面问题的罪魁祸首
为了下面更好的理解互斥问题,我们这里给大家提几个简单的概念:
如果++是一个原子性的操作的话,那么就不会出现上述的问题,所以现在要想解决上面的问题要么将上述代码实现原子性操作,要么实现互斥的机制,Linux中确实提供了原子的++操作(atomic)但是这仅仅解决了当前问题,所以我们这里重点介绍线程的互斥锁实现互斥。
为了解决上面的问题,我们需要做到3点:
要做到这三点,本质上需要一把锁,Linux下提供了一把锁叫做互斥量:
接下来我们先学习互斥量的接口,然后再使用互斥量解决我们上述的问题
互斥量的接口相对来说比较简单,我们将所有的接口一次性介绍完,用一个修改后的上文代码来做演示,这样便于读者们观察。
初始化互斥量的两种方式
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER
int pthread_mutex_init(pthread_mutex_t *restrict mutex, const pthread_mutexattr_t*restrict attr);
//参数一为要初始化的互斥锁,参数二一般设置为NULL表示使用默认属性
互斥量的销毁
int pthread_mutex_destroy(pthread_mutex_t *mutex);
使用PTHREAD_ MUTEX_ INITIALIZER初始化的互斥量不需要销毁,不要销毁一个已经加锁的互斥量已经销毁的互斥量,要确保后面不会有线程再尝试加锁
互斥量的加锁和解锁
int pthread_mutex_lock(pthread_mutex_t *mutex);
int pthread_mutex_unlock(pthread_mutex_t *mutex);
调用pthread_ lock 时,可能会遇到以下情况:
互斥量处于未锁状态,该函数会将互斥量锁定,同时返回成功
发起函数调用时,其他线程已经锁定互斥量,或者存在其他线程同时申请互斥量,但没有竞争到互斥量,那么pthread_ lock调用会陷入阻塞(执行流被挂起),等待互斥量解锁
现在我们来使用互斥量将上面程序修改成一个线程安全的程序:
这个修改的程序当线程一要对goal进行++时,如果当前没有线程再临界区中,那么线程一拿到锁并加锁然后进行临界资源的访问,当线程二要访问临界资源时发现锁资源无法被申请,所以操作系统将线程二设置为非r状态并将其加入到阻塞队列中等待线程一释放锁资源。这样就可以保证在线程对临界区访问时同一个时间只有一个线程存在,也就是说我们现在可以认为我们的程序++是原子的。
但是有的同学提出了这样的一个问题,尽然互斥锁是用来解决原子性问题的,那么互斥锁本身是怎么保证加锁是原子的呢?
互斥锁的实现原理实际上非常的简单,他并没有你想的那么复杂,这里我们必须要知道的前提是我们可以将汇编的一句指令认为是原子的,所以为了实现锁的互斥操作,大多数体系结构对于互斥锁的实现使用了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;
我们保证mutex中的1在整个程序中只有一份,下图会更加加深你对互斥量实现原理的机制:
我们上面加加变量的小栗子有问题其实另一个原因是他本质是一个不可重入的函数,什么是重入?重入是指同一个函数被不同的执行流调用,当前一个流程还没有执行完,就有其他的执行流再次进入,我们称之为重入。一个函数在重入的情况下不会出现不同或者任何问题,则我们称其为可重入函数,否则反之称为不可重入函数。
常见不可重入的情况:
常见可重入的情况:
线程安全是指多个线程并发执行一段代码时,不会出现不同的结果。常见对全局变量或者静态变量进行操作,如果没有互斥锁的保护的情况下,会出现该问题。
常见线程不安全的情况:
常见线程安全的情况:
Linux线程互斥是一种非常重要的机制,同学们不仅要明白这种机制产生原因,并且需要学会解决线程安全的方案,而互斥锁的实现原理也需要大家牢记在心。