【秒杀概念】从进程到死锁

发布于:2022-12-16 ⋅ 阅读:(517) ⋅ 点赞:(0)

目录

进程

线程

线程与进程的区别

锁策略

乐观锁:

悲观锁:

读写锁:

重量级锁:

轻量级锁:

自旋锁:

挂起锁:

公平锁:

非公平锁:

不可重入锁:

可重入锁:

死锁

一个线程一把锁:

两个线程两把锁:

多个线程多把锁:

综上,当死锁问题出现时,都伴随着以下4点:

互斥使用:

不可抢占:

请求保持:

循环等待:


进程

计算机操作系统将一个运行中的应用程序称为一个进程,操作系统需要为进程分配计算机资源,起到防止硬件设备被应用程序滥用和为应用程序提供一套简单的接口机制用来控制复杂而又被多次使用的低级硬件设备。

进程是计算机系统上可执行文件运行后的形态,我们在电脑上看到的.exe文件就是可执行文件,当我们双击它的时候,它的关键性息就会加载到计算机的内存中,并且开始运行其中的代码,于是就形成了一个进程,计算机将会为它分配CPU、内存、磁盘、网络等资源。

进程是操作系统分配资源的基本单位,计算机会为每一个进程分配系统资源,使其可以利用资源完成特定的操作。

在计算机中,需要有一种数据结构去保存每一个进程的相关信息,这样计算机才能识别每个进程需要的内存大小等,并且通过这个数据结构去运行进程,为它们调用底层的硬件设备。这种数据结构类似于一个双向链表,而每个进程就被抽象为一种结构体,这种结构体被称为“进程控制块抽象”(Process Contorl Block),简称PCB。PCB作为计算机对进程的管理单位,其中包含一个进程的核心信息,如进程身份标识pid、内存指针、文件描述符表等。

在我们的电脑上有许多进程在同时运行,它们都是链表中的一个元素,假设我们的电脑是单核单CPU的。所以这些进程都是在内存中排队,轮流使用CPU资源。

计算机系统的内存管理系统拥有内存分配、内存保护、内存映射、内存扩充等功能。主要任务是为多个进程的运行提供良好的运行环境,为进程分配好内存,使其能流畅运行,每个进程都会分到一块内存区域,进程与进程之间互不干扰。

进程拥有一些自身的属性,这些属性决定了CPU调度的循序,它们分别为进程优先级、进程状态、记账信息、上下文等。操作系统会综合考虑优先级、就绪或阻塞、过往的执行信息等决定是否让此进程上CPU执行。

 

线程

线程为计算机系统执行的基本单位,也就是说线程是上CPU的最小单位,每个线程就是一个“执行流”。单核的CPU算力有限,所以随着计算机科学技术的发展,CPU就发展出了多个核心,所以我们可以让多个任务并发的进行,更充分地利用多核CPU资源。因为线程比进程更加轻量,所以线程的创建、销毁和调用更快,更适合并发编程。

 

线程与进程的区别

首先,进程包含线程,进程要比线程高上一级,每个进程至少有一个线程存在,称为主线程。其次,上文提到每个进程之间是不共享内存空间的,但进程中可能会含有多个线程,而这些线程之间是一同共享一块内存空间的。进程是系统分配资源的的最小单位,线程是系统调度的最小单位。我们可以将进程比作是一个木材加工厂,原木作为原料生产出木板,即进程使用系统资源完成任务;线程就是工厂中的流水线,进行一些细节的加工操作。原材料都是以工厂为单位配送的,工厂中的流水线都是共用这些木材的,每一条开启了的流水线都是同时进行工作的。

上文提到线程与进程相比更轻量,通过以上的例子就能推断出原因了。当我们新建一个进程的时候就如同用新建一个工厂,需要购买新的厂房,新的设备和员工;而新增流水线就只需要添加新的车床等机器设备就行了,大大减少了资金等方面的开销。即,新建一个线程不需要像新建进程那样,让操作系统分配新的内存资源和创建新的PCB,它是直接在当前进程的内存中创建的,大大减少了系统开销。

 

现在,我们假设在这个木材加工厂中每条条流水线都配有一个操作员,操作员负责机器的开启和关闭,并且他们有一个可以修改仓库库存的程序,每当他们完成一次生产,就需要手动在程序上添加一批次的库存信息。现在工厂有10条流水线,并让每条流水线生产10批次的木材。在这种情况下,如果有人在一另一个人,还没退出修改库存数量的操作界面时也进入操作系统中进行库存数量的修改,这时就会出现读写的线程安全问题。如,1号操作员在50批的时候进入修改界面打算修改为51,同时2号操作员也是在50批的时候进入了修改界面,他也打算修改为51。这时1号操作员修改完成了,然后2号操作员也完成了修改,本来仓库中应该已经有了52批的,但现在只显示51批,这就是线程读写过程中容易出现的安全问题。

出现了这样的问题,工厂老板连夜找人修改程序,解决的办法很简单,就是当有人进入修改界面时,就不允许有人在进入修改界面进行修改,直到第一个人完成修改才可以重新进入修改界面。这个禁止和重新允许的方式就称为加锁。

当我们加锁时,就可以确保加锁后的操作的原子性、循序性和可见性,即把需要保证一体性的多个操作加在一把锁下,就能使这些操作成为一个不可分割的原子操作等。

 

锁策略

加锁后的效果也取决于加锁的方式,这些方式也被称为锁策略。常见的锁策略有:乐观锁、悲观锁、读写锁、重量级锁、轻量级锁、自旋锁、公平锁、非公平锁、可重入锁、不可重入锁等等。

乐观锁:

认为当前锁的申请不用耗时很长时间,所以选择在用户态中完成锁功能。

悲观锁:

认为当前很难申请到锁,因此选择将线程放入内核中挂起,产生阻塞等待。

读写锁:

在我们日常工作中,一个数据被读的次数远多于修改的次数。因此我们在多线程中只读数据时,不需要频繁地申请和释放锁。而操作系统内核API中提供的读写锁就把加锁操作分成加读锁和加写锁。当有两个线程同时对一个对象加写锁时,会发生锁竞争;当两个线程同时加读锁时,不产生竞争,提高程序效率;当一个线程读一个线程写时,也使其发生锁竞争。

重量级锁:

一般是依赖于操作系统提供的锁,在使用这种锁的时候,容易产生阻塞等待。

轻量级锁:

避免使用操作系统提供的锁,而是选择尽量在用户态来完成功能。尽量避免用户态和内核态之间的切换,从而避免线程被挂起等待。

自旋锁:

轻量级锁的具体实现方式,类似于死循环,加锁失败后还会立刻去竞争,直到获取锁成功,缺点是不断的获取锁会占用CPU资源。

挂起锁:

重量级锁的具体实现方式,加锁失败后,选择挂起等待,重新等待系统给分配CUP资源。

公平锁:

公平的规则为“先来后到”,先申请锁的线程,在之前的锁释放后,可以先获取到锁,后来的没有机会获取到这次释放的锁。

非公平锁:

一般操作系统都使用这种锁策略。不管你是先来还是后到,在上一个锁释放后,都一同竞争这次释放的锁,谁抢到算谁的。

不可重入锁:

当一个线程在拥有锁的状态下再次申请锁,如果是不可重入锁,那就会发生死锁。原因是第二个锁申请成功需要等待第一个锁的释放,而第一个锁的释放又需要让第二个锁获取锁成功后并向后执行到所释放后,第一个锁才能进行

可重入锁:

为了避免上述死锁问题,于是就有了可重入锁。当出现上述情况的时候,会通过内部的记录查询上锁的对象。如果是同一个对象申请锁直接让第二个锁申请成功,并且所内部还会有计数器记录当前对象上锁的个数,在释放锁时,让计数器减1,归零时就代表这个对象所有的锁都释放完成了。

 

死锁

当我们在多线程编程中,总会写出写bug,这些bug很可能会引起死锁。上文提到了一个线程一把锁的死锁情况。还有两个线程两把锁的情况与多个线程多把所锁的情况。

一个线程一把锁:

一个线程连续对一个对象加两次不可重入锁。

两个线程两把锁:

线程A获取到锁1:线程B获取到锁2。当线程A再尝试获取锁2,线程B再尝试获取锁1时,就会发生死锁。因为线程A想要锁2就得等线程B的锁释放,而线程B的锁释放有需要线程A的锁释放,但A又在等B,互相循环等待,所以就形成了死锁。

多个线程多把锁:

    经典问题:哲学家就餐。5个哲学家(倔脾气老头)一起去吃意大利面,本来应该给每一位哲学家分一双筷子,但是聚会的地方就只找到5支筷子,他们围着圆桌落座,将每支筷子放在两人之间,类似于下图

哲学家需要凑齐一双筷子才能进餐,否则就会开始思考人生。当每个哲学家都用左手拿了放在左边的筷子时,其中任意一个哲学家再尝试获取右边的筷子时,就发现已经被人拿了,而且哲学家脾气都挺倔的,他也不放下筷子就独自思考起了人生。这种状况出现时,这位哲学家A(第一个尝试获取右边筷子的哲学家)的下一个哲学家B也尝试获取A的筷子1,也开始了对“生而为人,我该何去何从”的思考,CDE都是如此。这就形成了一个循环的等待环路,也就造成了死锁。

 

综上,当死锁问题出现时,都伴随着以下4点:

互斥使用:

当锁被一个线程占有时,别的线程就不能再获取到这把锁

不可抢占:

一个线程占有锁时,其他线程没法抢走它的锁,只能等它主动释放掉这把锁,别的线程才能尝试获取。

请求保持:

当线程想要获取多把锁时,在获取新的锁失败后,原有的锁也不会被放弃,线程将一直保持占有原先已经成功申请到的锁。

循环等待:

就像上文的,一个线程自己等自己循环等待、两个线程相互循环等待、多个线程形成循环的等待环路。

因此当以上者四个条件都成立时,便形成了死锁;而前三个条件一般都是锁的特性,不好更改,所以我们就用第四个条件来解决死锁问题。

如何破坏循环等待,我们可以使用对锁编号,让获取锁有优先级。规定线程获取多把锁时,只能从小编号的锁开始获取。这样当一个线程想同时获取第一把锁1和最后一把锁n时,就需要先获取到锁1才能再获取锁n。像上文哲学家死锁的情况下,最后一个哲学家E就不会先选择锁5,而是会先尝试获取锁1,而锁1被哲学家A占着,所以E就开始了哲学思辨,这时哲学家D尝试获取锁5就可以成功了,于是DCBA相继用完餐,最后A把锁1给了E,大家就都吃完了美味的意大利面。所以,当我们给锁编号后,最后一个线程就不会获取最后一把锁导致循环等待环路出现了。

 

本文含有隐藏内容,请 开通VIP 后查看