Linux操作系统学习(互斥)
创始人
2024-05-29 12:09:00
0

文章目录

    • 线程安全
    • 互斥量
    • 互斥锁的原理
    • 线程安全补充
      • 可重入函数
      • 死锁

线程安全

​ 由于多个线程是共享同一个地址空间的,也就是很多资源都是共享的,那么线程通信就会很方便,但是方便的同时缺乏访问控制,可能会由于一个线程的操作问题,导致其他线程异常、崩溃、逻辑不正确等问题,这就是线程安全问题

​ 例如多个线程同时使用printf函数实际上在共享stdout资源,而stdout资源只有一个,多个线程都在使用它就可能导致打印出现乱码现象,只要涉及到全局的数据就会有线程安全问题。

下面设计一个抢票逻辑来验证一下线程安全问题

#include 
#include 
#include 
#include //抢票逻辑,1000张票,设5个线程同时抢
//tickets就是临界资源
//线程 在时间片到了、从内核态返回用户态时会进行切换
int tickets = 1000;void* ThreadRun(void* args)
{int id = *(int*)args;delete (int*)args;while(true){if(tickets > 0){usleep(10000);std::cout << "线程[" << id <<"]正在抢票. . .剩余票数:" << tickets << std::endl;  tickets--;}elsebreak;}
}int main()
{pthread_t tid[5];for(size_t i = 0;i < 5;i++){int* id = new int(i);pthread_create(tid+i,nullptr,ThreadRun,(void*)id);}for(size_t i = 0;i < 5;i++)pthread_join(tid[i],nullptr);return 0;
}

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ZZmIa8Be-1677869555313)(G:\Typora\图片保存\image-20221224160153268.png)]

​ 上面的执行结果发现,最后抢票抢到了负数,很明显是出现了线程安全问题,实际中是绝对不能出现抢票抢到负数、两个人买到同一个票的问题

为什么会出现这种现象?

​ 当多个进程竞争CPU的时候,CPU为了保证每个进程能公平被调度运行,采取了处理任务时间分片的机制,轮流处理多个进程,每个进程都执行一段时间后切换至下一个进程不断循环直到执行结束,由于CPU处理速度非常快,在人类的感官上认为是并行处理,实际是伪并行,同一时间只有一个任务在运行处理。

​ 所以每个task_struct被cpu调度都是有时间片的,当线程1被cpu调度后时间片开始计时,同时cpu中的寄存器会产生线程1的上下文数据,当时间到了后,寄存器会记录线程1执行到哪,线程1会存储这些数据,下一次执行的时候再加载至寄存器中继续执行。

​ 假如票就剩1张了,线程1刚执行完票数减的代码时间片就到了就被切换至线程2;当线程1再一次到达运行队列顶端后加载他的数据到CPU中,寄存器会根据上下文数据,接着上一次的代码结束位置继续执行,那么此时线程1认为票数剩一张执行的自减,而实际是在线程1被切换下来的时候,ticktest又被线程2自减了1次,但是线程1再次被调度是接着上一次的代码结束的位置继续执行的,所以就出现了抢到负数的现象

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-WaU348c7-1677869555313)(G:\Typora\图片保存\image-20221224172316557.png)]

互斥量

上述抢票逻辑解决方案:对临界区的资源进行保护

方式:互斥

互斥:在任意时刻,只允许一个执行流访问临界区(即对临界区加互斥锁)

临界资源:像打印数据到显示器这样的就可以看作是临界资源,tickets是全局变量,就是一种临界资源

临界区:访问临界资源的代码区域

pthread库提供了互斥锁的相关函数,如下所示:

  • 初始化互斥量

    int pthread_mutex_init(pthread_mutex_t *restrict mutex,const pthread_mutexattr_t *restrict attr);
    方式2:使用宏初始化 
    pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER
    
    • pthread_mutex_t:互斥量的数据类型,需要在全局(临界区)定义一个此类型的变量
    • restrict mutex:要初始化的的互斥量
    • restrict attr:相关属性设置,一般设置NULL交给OS去设置
    • 返回值:成功返回0,失败返回错误码
  • 销毁互斥量

    int pthread_mutex_destroy(pthread_mutex_t *mutex);
    
    • 销毁互斥量

    • 注意:

      ​ 使用静态分配PTHREAD_MUTEX_INITIALIZER初始化的互斥量,不需要销毁

      ​ 不要销毁一个已经加锁了的互斥量

      ​ 已经销毁了的互斥量,要确保后面不会有线程再加锁。

    • 返回值:成功返回0,失败返回错误码

  • int pthread_mutex_lock(pthread_mutex_t *mutex);
    int pthread_mutex_lock(pthread_mutex_t *mutex);
    
    • lock:加锁,线程调用该函数让互斥量上锁,如果该互斥锁已被另一个线程锁定和拥有,则调用此函数的该线程将阻塞,直到该互斥锁变为可用为止
    • unlock:解锁,解除锁定 mutex 所指向的互斥锁
    • 返回值:成功返回0,失败返回错误码

上述代码变化如下:

class ticket
{
public:ticket():tickets(1000){pthread_mutex_init(&mtx,nullptr);//初始化锁}~ticket(){pthread_mutex_destroy(&mtx);	//销毁锁}bool GetTicket(){bool ret = true;				//ret不是临界资源pthread_mutex_lock(&mtx);		//加锁if(tickets > 0){usleep(1000);std::cout << "线程[" << pthread_self() << "]正在抢票. . .剩余票数:" << tickets << std::endl;  tickets--;}else{std::cout << "剩余票空" << std::endl;ret = false;}pthread_mutex_unlock(&mtx);		//解锁return ret;}
private:int tickets;pthread_mutex_t mtx;		//创建锁
};void* ThreadRun(void* args)
{ticket* id = (ticket*)args;	//传入的是同一个对象,所以该对象是临界资源while(true){if(!id->GetTicket())	//票余量空则退出break;}
}int main()
{pthread_t tid[5];ticket* id = new ticket();//创建线程for(size_t i = 0;i < 5;i++)pthread_create(tid+i,nullptr,ThreadRun,(void*)id);//线程等待for(size_t i = 0;i < 5;i++)pthread_join(tid[i],nullptr);return 0;
}

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-BIBK658a-1677869555314)(G:\Typora\图片保存\image-20221225012034475.png)]

​ 上述代码在堆区申请了一个ticket类,又把首地址做为参数传给函数,所有线程访问的ticket就是同一个了(临界资源),在 pthread_mutex_lock(&mtx);[.....] pthread_mutex_unlock(&mtx);之间的执行流就是互斥的,串行执行。

但是要注意: GetTicket函数中的bool变量不是临界资源,它是在栈区的,谁使用谁创建,函数执行完后销毁

互斥锁的原理

要访问tickets,就要先访问mtx,mtx需要被所有线程看到,那么锁也是一种临界资源,如何保证锁的安全呢?

下面用一段加锁/解锁伪代码来解释互斥锁的原理:

lock:   movb 	$0  %al				//线程A、B在CPU上运行时,情况al寄存器xchgb 	%al mutex			//线程A把mutex与寄存器内容交换,线程A运行时CPU的al寄存器就有个对应数据if(al寄存器内容 > 0)			 //线程B运行时虽然也进行交换,但是mutex为0,交换完的al还是0{									return 0;//加锁成功		 //线程A继续往下执行时检查它在CPU运行时的al寄存器判定有锁}							//线程B继续往下执行时,检查它在CPU上运行时,al寄存器没对应数据,即无锁,将该线程else						//挂起等待(PCB被挂到等待队列){							//即便CPU时间片到了中途切换走:A运行时的,会把它在寄存器中相关数据存储 抱着锁走的//挂起等待				 //B也同理,所以在下一次CPU调度A或B运行时,只需要检测al寄存器的数据}							//而其他线程、后来创建的线程,无论如何也拿不到锁:goto lock					//1.mutex是全局的,已经由A在拿锁是交换为0了,在后续判断就会被判定无锁,随后被挂起//2.A在cpu上运行的时间片到了,也会抱着锁走的(存储自己运行时寄存器产生的数据)
unlock:movb 	$1 mutex			//只能等A解锁,重置mutex的值,并唤醒等待的线程(处于S状态的PCB)唤醒等待线程return 0
  • al是寄存器,被多个线程共享,但是数据不是共享的

    ​ CPU有一组寄存器,其中就有al,al可以说是共享的,但是al的数据是不一样的;每个线程被CPU调度后,al寄存器会产生相应的数据,当线程的时间片到了以后数据会被线程存储,下个线程被调度后,又会把它的数据加载到寄存器中。所以每个线程在被调度时都会有al寄存器,但是他们的数据是私有的。

  • 伪代码中的xchgb可以看成是swap或者exchange

    ​ 为了实现互斥锁操作,大多数体系结构都提供了swap或exchange指令,该指令的作用是把寄存器和内存单元的 数据相交换,由于只有一条指令,保证原子性

    • 原子性:不会被任何调度机制打断的操作,该操作只有两态,要么完成,要么未完成

加锁:

  • 线程执行加锁函数时,执行到movb $0 %al,把al寄存器清0

  • 之后会把内存当中的变量mutex 与 寄存器al进行数据交换(mutex一开始是1)

  • 之后进行对寄存器进行判断,判断是否为0,不为0则表示加锁成功,为0则表示锁被占用,把当前竞争锁的线程挂起等待

  • 中途若是时间片到了,是会把al寄存器的内容一起存储的,也就是”抱着锁被走的“,下一次被调度又会把数据重新加载到寄存器

解锁:

  • 当拿到锁的线程执行解锁函数时,先把mutex重新置1,其他的线程会被唤醒等待之后会经goto跳转到第一个行重新执行

注意:

  1. 带锁执行是比不带锁执行要慢一些的,因为加锁解锁也需要时间

  2. 只要有一个线程拿到锁,其他线程就会被挂起等待,除非拿到锁的线程解锁唤,否则其他线程是无法访问临界区的,从而保证了线程安全

  3. 所以为什么加锁函数可以保证原子性,主要是因为,核心争锁的部分是这一条交换语句 xchgb %al mutex,只会出现执行了这条语句和没执行这条语句的情况;

    • 看谁先执行这条语句,谁就拿到了锁,就算执行完后时间片到了或者被中断了那也是已经争到了锁。

    而自己设置个全局变量,利用++ 、-- 赋值等操作模仿加锁是不行的,这些操作没有原子性。

    • 例如++,在汇编层面需要三条语句才能全部执行完++的逻辑功能;若一个线程执行时中途发生中断,下一次接着中断位置执行完,而其他线程在中断期间执行过++了,可能全局变量的数据就异常了。

线程安全补充

​ 多个线程并发同一段代码时,不会出现不同的结果。常见于全局变量或者静态变量进行操作,并且没有锁保护的情况下,会导致线程不安全。例如多个线程打印hello 可能会出现乱序

线程安全的情况:

  1. 每个线程对于全局会在静态变量只有读权限,没有写权限

  2. 类或者接口对于线程来说时原子的

  3. 多线程切换不会导致结果出现二义性。

    反之就是不安全的

可重入函数

可重入函数:同一函数被不同执行流调用,当前线程还没执行完,就有其它进程进入,我们称之为重入。一个函数在重入的情况下,运行结果不会有任何问题,则该函数称为可重入函数,否则就是不可重入函数。

不可重入函数如:malloc、new、free、io操作的相关函数等

可重入函数与线程安全的问题:

  1. 函数是可重入的,那就是线程安全的
  2. 函数是不可重入的,那就不能由多个线程使用,有可能引发线程安全问题
  3. 如果一个函数中有全局变量,那么这个函数既不是线程安全也不是可重入的

死锁

​ 死锁是指在一组进程中的各个进程均占有不会释放的资源,但因互相申请被其他进程所站用不会释放的资 源而处于的一种永久等待状态。

例如,拿了锁却没释放,之后又去申请锁,导致这个有锁的线程也挂起等待,从而变成永久等待

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-meZYrcEk-1677869555314)(G:\Typora\图片保存\image-20230207184534035.png)]

死锁的必要条件:

  • 不可剥夺(不能改):执行流获取了互斥锁之后,除了自己主动释放锁,其他执行流不能解该互斥锁

  • 循环等待:线程A等待线程B拿的锁,线程B等待线程A拿的锁

  • 互斥条件(不能改):一个互斥锁,只能被一个执行流在同一时刻拥有

  • 请求与保持:线程A拿着 1 锁还想请求 2 锁,线程B拿着 2 锁还想请求 1 锁

只有同时满足上述4点,才会导致死锁;破坏死锁的其中一个必要条件即可避免

相关内容

热门资讯

简历特长爱好怎么写 简历特长爱好怎么写  简历特长爱好怎么写,往往的情况下面,我们会把简历中的特长和爱好写在同一个板块中...
应届生个人简历 应届生个人简历(精选10篇)  个人简历是求职者给招聘单位发的一份简要介绍。下面是小编收集整理的应届...
个人简历自荐信 个人简历自荐信范文(精选10篇)  导语:在如今这个时代,我们越来越经常使用自荐信,我们在写自荐信的...
新员工入职简历表 新员工入职简历表  时间一晃而过,眼见着,找工作的时间马上到来,让我们一起来学习写简历吧。简历怎么写...
个人简历的自我评价怎么写 个人简历的自我评价怎么写本人性格开朗、稳重、有活力,待人热情、真诚;工作认真负责,积极主动,能吃苦耐...
社区工作者空白个人简历表格下... 该内容已删除或未通过审核,请返回首页浏览其它页面,感谢你的理解,谢谢。
美术教师招聘简历模板免费下载   美术:泛指创作占有一定平面或空间,且具有可视性的艺术,就叫作美术。它的划分有多种,一般地包括四大...
护士个人简历封面 护士个人简历封面  如何撰写自己的简历,让招聘决策者过目不忘并留下深刻印象,从而为你带来面试机会,是...
服装设计师简历 服装设计师简历  求职者要想求职成功必须要能写出一份优秀的个人简历来,要如何写好简历呢?下面公文站小...
最新简历个人评价   个人评价相信大家都接触到,工作过程中自我评价切记长篇大论,以下这篇是小编整理的“最新简历个人评价...
研究生个人简历 研究生个人简历范文  求职简历求职中,个性突出、特征鲜明的求职者容易在竞争中取胜,而简历也需要个性突...
护士求职个人简历表格   CN人才网小编给大家整理的一篇护士求职个人简历表格,一起来看看吧。姓 名:xxxx 性 别: 男...
应聘个人简历自我介绍 应聘个人简历自我介绍范文(精选5篇)  来到一个完全陌生的环境,我们通常会被要求作自我介绍,自我介绍...
个人的简历word格式 个人的简历范本word格式  时间过得飞快,前方等待着我们的将是新的工作机会和挑战,你的简历写好了吗...
平面设计师个人简历 平面设计师个人简历(精选7篇)  时间过得可真快,从来都不等人,我们找工作的时间越来越近,这时候需要...
蓝色简约风word个人简历模...   出生年月: 联系电话 :12345678  电子邮箱:XXX.com 通信地址 :xx省xx市 ...
员工入职登记表   导读:作为人力资源部来说,招聘和员工登记一定是很重要的工作,企业招聘新员工时,往往会让他们填写一...
新媒体运营个人简历 新媒体运营个人简历范文  时间一晃而过,找工作对于我们说已越来越近,需要为此写一份简历了哦。千万不能...
最新个人简历范文优秀篇   当简历具有“能否有第一次见面机会”的决定权时,简历上的细节部分也被格外重视起来。以下是小编推荐的...