【Linux系统】深入理解线程,互斥及其原理
前言:
上文我们讲到了线程的概念与控制【Linux系统】初见线程,概念与控制-CSDN博客
本文我们再来讲讲,线程的下一部分内容:线程的互斥与同步!
点个关注!
线程互斥
在上一文章中我们讲到了线程的概念与控制,知道了线程是共享进程中的绝大部分资源的!但是当多个线程同时访问同一个公共资源时,就会出现一个问题:数据不一致!
为解决数据不一致问题,我们就想要再深入理解线程中的互斥与同步!
互斥相关概念
临界资源:多线程执行流中被保护的共享资源就叫做临界资源
临界区:每个线程内部,访问临界资源的代码区,就叫做临界区
互斥:对临界资源起保护作用!保证任何时候,有且只有一个执行流进入临界区,访问临界资源!
原子性:表示一个操作拥有两态:要么完成,要么没做,不存在做了一半的情况!这就表示这个操作拥有原子性!不会被任何调度打断当前任务进行的性质!
互斥
数据不一致现象
//模拟抢票流程#include <iostream>
#include <stdlib.h>
#include <string>
#include <unistd.h>
#include <pthread.h>
using namespace std;int ticket = 1000;void *route(void *args)
{string name = static_cast<char *>(args);while (true){if (ticket > 0){usleep(1000);cout << name << ":sells ticket:" << ticket << endl;ticket--;}elsebreak;}return nullptr;
}int main()
{pthread_t t1, t2, t3, t4;pthread_create(&t1, NULL, route, (void *)"thread 1");pthread_create(&t2, NULL, route, (void *)"thread 2");pthread_create(&t3, NULL, route, (void *)"thread 3");pthread_create(&t4, NULL, route, (void *)"thread 4");pthread_join(t1, NULL);pthread_join(t2, NULL);pthread_join(t3, NULL);pthread_join(t4, NULL);
}
我们发现,票竟然被抢到了负数!可明明我们的判断条件是 ticket > 0 啊?
这就是多线程下,数据不一致的问题!也叫做线程安全问题!
用锁解决数据不一致问题
// 用锁解决,数据不一致问题(pthread锁)
int ticket = 1000;
pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER; // 定义并初始化锁void *route(void *args)
{string name = static_cast<char *>(args);while (true){// 对临界区上锁pthread_mutex_lock(&lock);if (ticket > 0){usleep(1000);cout << name << ":sells ticket:" << ticket << endl;ticket--;// 结束访问临界区,解锁pthread_mutex_unlock(&lock);}else{// 结束访问临界区,解锁pthread_mutex_unlock(&lock);break;}}return nullptr;
}int main()
{pthread_t t1, t2, t3, t4;pthread_create(&t1, NULL, route, (void *)"thread 1");pthread_create(&t2, NULL, route, (void *)"thread 2");pthread_create(&t3, NULL, route, (void *)"thread 3");pthread_create(&t4, NULL, route, (void *)"thread 4");pthread_join(t1, NULL);pthread_join(t2, NULL);pthread_join(t3, NULL);pthread_join(t4, NULL);
}
通过锁的方式,对临界区进行保护,解决数据不一致的问题!
理解为什么数据会不一致
首先,我们要知道ticket--操作,对于计算机来说并不是原子的!为什么呢?因为ticket--的操作对于计算机来说并不是一个步骤,而是3个步骤。
一条汇编语句当然是原子的,因为只存在做了或者没做的情况。但多条的汇编语言,一般都是不具有原子性的!正如图中的ticket--操作一样!
ticket--操作分为3步:第一步,将ticket的值加载到CPU的运行寄存器ebx中。第二部,运算寄存器ebx进行减一操作,将1000减为999。第三步,将ebx中的内容写回至内存中的ticket。由此完成了ticket--操作。
理解了ticket--操作不是原子的,下面我们再来看看:
我们都知道线程是有时间片的,当时间耗尽时,线程会保存上下文数据,并被剥离出CPU!
那么,当一个线程执行上述代码时。因为ticket--操作不是原子的,所以ticket--操作的3个步骤没执行完就被剥离走是常见的情况!那么当一轮执行完成后,没有执行完的线程又会回来继续执行,但没有执行完的线程的上下文数据会直接覆盖CPU中的寄存器,接着执行。这意味着,在这个线程之前的线程所做的一切都没有意义,会被当前这个线程之间覆盖掉!!!
举例理解:假设线程有一线程a,执行到运算寄存器ebx减1后,时间片耗尽线程的上下文数据被保存后被剥离!新的线程进来,执行线程操作。假设后续代码一切顺利执行,成功的将ticket减到了900。当旧线程重新被CPU调度时,旧线程的上下文数据覆盖CPU中的寄存器数据,继续执行还没有执行的代码,此时旧线程已经将运算寄存器ebx中的内容减1了,下一步将ebx的中内容写回ticket中!此时ticket中的900被覆盖为999!着也就意味着之前的线程说做的事是无意义的了!
综上所述,这个就是数据不一致问题的原因!
理解为什么会变成负数
在多线程中,存在多个线程同时通过if判断,进入临界区中,访问临界资源的情况!
那如果当ticket此时为1,被多个线程执行减一操作,那ticket不就变为负数了吗!!!
所以在此代码中,存在严重问题!数据不一致问题 + "超售"。
认识锁的作用
锁的全称:互斥锁(又叫互斥量)
顾名思义,互斥锁。其作用就是互斥!保护临界区资源,保证同一时间有且只有一个线程可以进入临界区,访问临界资源!杜绝数据不一致问题!
锁接口(pthread库)
初始化互斥锁
全局初始化:pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;pthread_mutex_t是数据类型
PTHREAD_MUTEX_INITIALIZER是宏全局申请互斥锁,代码结束会自动释放
局部初始化锁:pthread_mutex_t mutex;int pthread_mutex_init(pthread_mutex_t *mutex, const pthread_mutexattr_t *attr);参数二:为属性指针,默认传nullptr
注意:局部初始化的锁想要我们自己手动的销毁int pthread_mutex_destroy(pthread_mutex_t *mutex);
申请锁与解锁
申请锁:
将已经初始化后的锁,传入给pthread_mutex_lock进行申请锁int pthread_mutex_lock(pthread_mutex_t* mutex)
解锁:
线程获得锁后必须解锁,保证后续的线程也可以进入临界区,访问临界资源int pthread_mutex_unlock(pthread_mutex_t* mutex);
申请成功:线程进入临界区,访问临界资源
申请失败:线程挂起,等待下一次的申请
锁提供的作用:将执行临界区的代码由并行转化为串行!保证线程执行期间不会被打扰。
理解:
如果线程不遵守锁的规则呢?
这将是一个bug!所有线程都必须遵守!
加锁之后,在临界区线程进行了切换会怎么样?
不怎么样。因为锁是被线程持有的,线程被切换了其申请的锁并没有解锁!这就意味着其他线程必须等待被切换的线程回来继续执行,直到退出临界区解锁。
形象理解:
现在有一个自习室,只能容纳一个人进入其中。
大家都来争抢锁,只有获得锁的人才能够进入自习室!在你获得锁的期间,没有任何人没有进入自习室,必须等待你归还钥匙,解锁后。再次获得锁的人才能够进入自习室。
在你获得锁的期间,即使你出去上厕所或者吃饭,只要你没有归还锁,这个自习室依然是只有你可以进入的。
锁实现原理
lock:movb $0, % alxchgb% al, mutexif (al寄存器的内容 > 0){return 0;}else挂起等待;goto lock;unlock:movb $1, mutex唤醒等待Mutex的线程;return 0;
锁是为了保护临界资源的,让线程申请锁。申请到锁的线程才可以进入临界区,访问临界资源,保证了同一时间有且只有一个线程进入临界区。
但是我们会发现,锁只有一个,但是会有多个线程同时申请锁!此时锁不就变成临界资源了吗?要保证同一时间下锁不会被多个线程都申请到,就正如同保证临界资源不会同时被多个线程同时访问一样!
那如何解决呢?请看:实现锁的伪代码!
lock部分:
lock:movb $0, % alxchgb% al, mutexif (al寄存器的内容 > 0){return 0;}else挂起等待;goto lock;步骤:1.将寄存器al的值设置为02.交换寄存器al与mutex的值(xchg表示原子性操作)3.判断寄存器al中的值4.如果值大于0,则申请锁成功。反之失败,挂起等待goto lock:表示再次回到lock的第一行
注:初始化的锁mutex其值是1!
步骤1:重置为0
步骤2:交换
判断:寄存器al中的值大于1,成功申请锁,线程可以进入临界区,访问临界资源!
那么在锁已经被申请且没有解锁的前提下,其他线程再来申请锁会发生什么?
首先,该线程保存上下文并被剥离CPU,此时mutex已经被交换为0值了。然后新线程加载之CPU,进行重置为0、交换操作。
此时不论时寄存器al,还是mutex都是0值了,交换了之后依然是0值。所以判断当前这个新线程申请锁失败!
申请锁的操作很有意思,数据的流动不是拷贝 而是交换,这就保证了整个锁中只有一个1值,也就是最多只有一个线程能够获得锁!
unlock部分:
unlock:movb $1, mutex唤醒等待Mutex的线程;return 0;步骤:1.将mutex赋值为12.唤醒之前因申请失败而挂起等待的线程
当前线程已经不再需要访问临界资源了,需要将锁还给内存,让其他等待的线程可以申请锁。
解锁的步骤很简单。直接将mutex赋值为1即可!当新的线程加载之CPU中时,寄存器al被赋值为0,进行交换后al再次为1,新线程申请锁成功!