并发编程
day 31
进程
一、什么是进程:
进程指的是正在运行的程序,是一系列过程的统称,也是操作系统调度和进行资源分配的基本单位。
进程是实现并发的一种方式,在学习并发编程之前要先了解进程的基本概念以及多进程的实现原理,这就必须提到操作系统了,因为进程这个概念来自于操作系统,没有操作系统就没有进程。
二、什么是并发编程:
并发指的是多个任务同时被执行,在之前的TCP通讯中,服务器在建立连接后需要一个循环来与客户端循环的收发数据,但服务器并不知道客户端什么时候会发来数据,导致没有数时服务器进入了一个等待状态,此时其他客户端也无法链接服务器,很明显这是不合理的,学习并发编程就是要找到一种方案,让一个程序中的的多个任务可以同时被处理。
三、多进程的实现原理-多道技术
1、操作系统介绍
下图是操作系统在整个计算机中所在的位置:
操作系统位于应用软件和硬件设备之间,本质上也是一个软件,
它由系统内核(管理所有硬件资源)与系统接口(提供给程序员使用的接口)组成
操作系统是为了方便用户操作计算机而提供的一个运行在硬件之上的软件
2、操作系统的两个核心作用
1、为用户屏蔽了复杂繁琐的硬件接口,为应用程序提供了,清晰易用的系统接口。
有了这些接口以后程序员不用再直接与硬件打交道了
例子:有了操作系统后我们就可以使用资源管理器来操作硬盘上的数据,而不用操心,磁头的移动啊,数据的读写等等
2、操作系统将应用程序对硬件资源的竞争变成了有序的使用。
例子:所有软件 qq啊 微信啊 吃鸡啊都共用一套硬件设备 假设现有三个程序都在使用打印机,如果不能妥善管理竞争问题,可能一个程序打印了一半图片后,另一个程序抢到了打印机执行权于是打印了一半文本,导致两个程序的任务都没能完成,操作系统的任务就是将这些无序的操作变得有序
3、操作系统与应用程序的区别
二者的区别不在于的地位,它们都是软件,而操作系统可以看做一款特殊的软件
1.操作系统是受保护的:无法被用户修改(应用软件如qq不属于操作系统可以随便卸载)。
2.大型:linux或widows源代码都在五百万行以上,这仅仅是内核,不包括用户程序,如GUI,库以及基本应用软件(如windows Explorer等),很容易就能达到这个数量的10倍或者20倍之多。
3.长寿:由于操作系统源码量巨大,编写是非常耗时耗力的,一旦完成,操作系统所有者便不会轻易的放弃重写,二是在原有基础上改进,基本上可以把windows95/98/Me看成一个操作系统。
发展历史:
多道技术出现在第三代操作系统中,是为了解决前两代操作系统存在的种种问题而出现,那么前两代操作系统都有哪些问题呢?一起来看看操作系统的发展历史:
第一代计算机(1940~1955):真空管和穿孔卡片
第一代计算机的产生背景:
第一代之前人类是想用机械取代人力,第一代计算机的产生是计算机由机械时代进入电子时代的标志,从Babbage失败之后一直到第二次世界大战,数字计算机的建造几乎没有什么进展,第二次世界大战刺激了有关计算机研究的爆炸性进展。
lowa州立大学的john Atanasoff教授和他的学生Clifford Berry建造了据认为是第一台可工作的数字计算机。该机器使用300个真空管。大约在同时,Konrad Zuse在柏林用继电器构建了Z3计算机,英格兰布莱切利园的一个小组在1944年构建了Colossus,Howard Aiken在哈佛大学建造了Mark 1,宾夕法尼亚大学的William Mauchley和他的学生J.Presper Eckert建造了ENIAC。这些机器有的是二进制的,有的使用真空管,有的是可编程的,但都非常原始,设置需要花费数秒钟时间才能完成最简单的运算。
在这个时期,同一个小组里的工程师们,设计、建造、编程、操作及维护同一台机器,所有的程序设计是用纯粹的机器语言编写的,甚至更糟糕,需要通过成千上万根电缆接到插件板上连成电路来控制机器的基本功能。没有程序设计语言(汇编也没有),操作系统则是从来都没听说过。使用机器的过程更加原始,详见下‘工作过程’
特点: 没有操作系统的概念 所有的程序设计都是直接操控硬件
工作过程: 程序员在墙上的机时表预约一段时间,然后程序员拿着他的插件版到机房里,将自己的插件板街道计算机里,这几个小时内他独享整个计算机资源,后面的一批人都得等着(两万多个真空管经常会有被烧坏的情况出现)。
后来出现了穿孔卡片,可以将程序写在卡片上,然后读入机而不用插件板
优点:
程序员在申请的时间段内独享整个资源,可以即时地调试自己的程序(有bug可以立刻处理)
缺点:
浪费计算机资源,一个时间段内只有一个人用。 注意:同一时刻只有一个程序在内存中,被cpu调用执行,比方说10个程序的执行,是串行的
第二代计算机(1955~1965):晶体管和批处理系统
第二代计算机的产生背景:
由于当时的计算机非常昂贵,自认很自然的想办法较少机时的浪费。通常采用的方法就是批处理系统。
特点: 设计人员、生产人员、操作人员、程序人员和维护人员直接有了明确的分工,计算机被锁在专用空调房间中,由专业操作人员运行,这便是‘大型机’。
有了操作系统的概念
有了程序设计语言:FORTRAN语言或汇编语言,写到纸上,然后穿孔打成卡片,再将卡片盒带到输入室,交给操作员,然后喝着咖啡等待输出接口
工作过程:
第二代如何解决第一代的问题/缺点: 1.把一堆人的输入攒成一大波输入, 2.然后顺序计算(这是有问题的,但是第二代计算也没有解决) 3.把一堆人的输出攒成一大波输出
现代操作系统的前身:(见图)
优点:批处理,节省了机时
缺点:1.整个流程需要人参与控制,将磁带搬来搬去(中间俩小人)
2.计算的过程仍然是顺序计算-》串行
3.程序员原来独享一段时间的计算机,现在必须被统一规划到一批作业中,等待结果和重新调试的过程都需要等同批次的其他程序都运作完才可以(这极大的影响了程序的开发效率,无法及时调试程序)
第三代计算机(1965~1980):集成电路芯片和多道程序设计
第三代计算机的产生背景:
20世纪60年代初期,大多数计算机厂商都有两条完全不兼容的生产线。
一条是面向字的:大型的科学计算机,如IBM 7094,见上图,主要用于科学计算和工程计算
另外一条是面向字符的:商用计算机,如IBM 1401,见上图,主要用于银行和保险公司从事磁带归档和打印服务
开发和维护完全不同的产品是昂贵的,同时不同的用户对计算机的用途不同。
IBM公司试图通过引入system/360系列来同时满足科学计算和商业计算,360系列低档机与1401相当,高档机比7094功能强很多,不同的性能卖不同的价格
360是第一个采用了(小规模)芯片(集成电路)的主流机型,与采用晶体管的第二代计算机相比,性价比有了很大的提高。这些计算机的后代仍在大型的计算机中心里使用,此乃现在服务器的前身,这些服务器每秒处理不小于千次的请求。
如何解决第二代计算机的问题1: 卡片被拿到机房后能够很快的将作业从卡片读入磁盘,于是任何时刻当一个作业结束时,操作系统就能将一个作业从磁带读出,装进空出来的内存区域运行,这种技术叫做 同时的外部设备联机操作:SPOOLING,该技术同时用于输出。当采用了这种技术后,就不在需要IBM1401机了,也不必将磁带搬来搬去了(中间俩小人不再需要)
如何解决第二代计算机的问题2:
第三代计算机的操作系统广泛应用了第二代计算机的操作系统没有的关键技术:多道技术
cpu在执行一个任务的过程中,若需要操作硬盘,则发送操作硬盘的指令,指令一旦发出,硬盘上的机械手臂滑动读取数据到内存中,这一段时间,cpu需要等待,时间可能很短,但对于cpu来说已经很长很长,长到可以让cpu做很多其他的任务,如果我们让cpu在这段时间内切换到去做其他的任务,这样cpu不就充分利用了吗。这正是多道技术产生的技术背景
多道技术:
多道技术中的多道指的是多个程序,多道技术的实现是为了解决多个程序竞争或者说共享同一个资源(比如cpu)的有序调度问题,解决方式即多路复用,多路复用分为时间上的复用和空间上的复用。
空间上的复用:将内存分为几部分,每个部分放入一个程序,这样,同一时间内存中就有了多道程序。
时间上的复用:当一个程序在等待I/O时,另一个程序可以使用cpu,如果内存中可以同时存放足够多的作业,则cpu的利用率可以接近100%,类似于我们小学数学所学的统筹方法。(操作系统采用了多道技术后,可以控制进程的切换,或者说进程之间去争抢cpu的执行权限。这种切换不仅会在一个进程遇到io时进行,一个进程占用cpu时间过长也会切换,或者说被操作系统夺走cpu的执行权限)
空间上的复用最大的问题是:程序之间的内存必须分割,这种分割需要在硬件层面实现,由操作系统控制。如果内存彼此不分割,则一个程序可以访问另外一个程序的内存,
首先丧失的是安全性,比如你的qq程序可以访问操作系统的内存,这意味着你的qq可以拿到操作系统的所有权限。
其次丧失的是稳定性,某个程序崩溃时有可能把别的程序的内存也给回收了,比方说把操作系统的内存给回收了,则操作系统崩溃。
多道技术案例:
生活中我们进程会同时做多个任务,但是本质上一个人是不可能同时做执行多个任务的,
例1:吃饭和打游戏,同时执行,本质上是在两个任务之间切换执行,吃一口饭然后打打游戏,打会儿游戏再吃一口饭;
例2:做饭和洗衣服,如果没有多道技术,在电饭煲做饭的时候我们就只能等着,假设洗米花费5分钟,煮饭花费40分钟,相当于40分钟是被浪费的时间。那就可以在煮饭的等待过程中去洗衣服,假设把衣服装进洗衣机花费5分钟,洗衣服花费40分钟,那么总耗时为 5(洗米)+5(装衣服)+40(最长等待时间) 大大提高了工作效率
多道技术也是在不同任务间切换执行,由于计算机的切换速度非常快,所以用户是没有任何感觉的,看起来就像是两个任务都在执行,但是另一个问题是,仅仅是切换还不行,还需要在切换前保存当前状态,切换回来时恢复状态,这些切换和保存都是需要花费时间的!在上述案例中由于任务过程中出现了等待即IO操作所以进行了切换,而对于一些不会出现IO操作的程序而言,切换不仅不能提高效率,反而会降低效率
例如:做一百道乘法题和做一百道除法题,两个任务都是计算任务是不需要等待的,此时的切换反而降低了运行效率!
第三代计算机的操作系统仍然是批处理
许多程序员怀念第一代独享的计算机,可以即时调试自己的程序。为了满足程序员们很快可以得到响应,出现了分时操作系统
如何解决第二代计算机的问题3:
分时操作系统: 多个联机终端+多道技术
20个客户端同时加载到内存,有17在思考,3个在运行,cpu就采用多道的方式处理内存中的这3个程序,由于客户提交的一般都是简短的指令而且很少有耗时长的,索引计算机能够为许多用户提供快速的交互式服务,所有的用户都以为自己独享了计算机资源
CTTS:麻省理工(MIT)在一台改装过的7094机上开发成功的,CTSS兼容分时系统,第三代计算机广泛采用了必须的保护硬件(程序之间的内存彼此隔离)之后,分时系统才开始流行
MIT,贝尔实验室和通用电气在CTTS成功研制后决定开发能够同时支持上百终端的MULTICS(其设计者着眼于建造满足波士顿地区所有用户计算需求的一台机器),很明显真是要上天啊,最后摔死了。
后来一位参加过MULTICS研制的贝尔实验室计算机科学家Ken Thompson开发了一个简易的,单用户版本的MULTICS,这就是后来的UNIX系统。基于它衍生了很多其他的Unix版本,为了使程序能在任何版本的unix上运行,IEEE提出了一个unix标准,即posix(可移植的操作系统接口Portable Operating System Interface)
后来,在1987年,出现了一个UNIX的小型克隆,即minix,用于教学使用。芬兰学生Linus Torvalds基于它编写了Linux
第四代计算机(1980~至今):个人计算机
第四代也就是我们常见的操作系统,大多是具备图形化界面的,例如:Windows,macOS ,CentOS等
由于采用了IC设计,计算机的体积下降,性能增长,并且成本以及可以被普通消费者接受,而第三代操作系统大都需要进行专业的学习才能使用,于是各个大佬公司开始开发那种不需要专业学习也可以快速上手的操作系统,即上述操作系统!
它们都是用了GUI 图形化用户接口,用户只需要通过鼠标点击拖拽界面上的元素即可完成大部分操作
四、进程与程序
进程是正在运行的程序,程序是程序员编写的一堆代码,也就是一堆字符,当这堆代码被系统加载到内存中并执行时,就有了进程。
例如:生活中我们会按照菜谱来做菜,那么菜谱就是程序,做菜的过程就是进程
需要注意的是:一个程序是可以产生多个进程的,就像我们可以同时运行多个QQ程序一样,会形成多个进程
测试:
import time while True: time.sleep(1)
多次运行该文件,就会产生多个python.exe进程,可以通过tasklist来查看运行的程序
PID和PPID
1、PID
在一个操作系统中通常都会运行多个应用程序,也就是多个进程,那么如何来区分进程呢?
系统会给每一个进程分配一个进程编号即PID,如同人需要一个身份证号来区分。
验证:
tasklist 用于查看所有的进程信息 taskkill /f /pid pid 该命令可以用于结束指定进程 # 在python中可以使用os模块来获取pid import os print(os.getpid()) #还可以通过current_process模块来获得 current_process().pid #可获得当前运行的程序的pid
2、PPID
当一个进程a开启了另一个进程b时,a称为b的父进程,b称为a的子进程
在python中可以通过os模块来获取父进程的pid
# 在python中可以使用os模块来获取ppid import os print("self",os.getpid()) # 当前进程自己的pid print("parent",os.getppid()) # 当前进程的父进程的pid
如果是在pycharm中运行的py文件,那pycahrm就是这个python.exe的父进程,当然你可以从cmd中来运行py文件,那此时cmd就是python.exe的父进程
五、并发与并行,阻塞与非阻塞
1、并发指的是,多个事件同时发生了。
例如洗衣服和做饭,同时发生了,但本质上是两个任务在切换,给人的感觉是同时在进行,也被称为伪并行
2、并行指的是,多个事件都是进行着
例如一个人在写代码另一个人在写书,这两件事件是同时在进行的,要注意的是一个人是无法真正的并行执行任务的,在计算机中单核CPU也是无法真正并行的,之所以单核CPU也能同时运行qq和微信其实就是并发执行
3、阻塞与非阻塞指的是程序的状态
阻塞状态是因为程序遇到了IO操作,或是sleep,导致后续的代码不能被CPU执行
非阻塞与之相反,表示程序正在正常被CPU执行
补充:进程有三种状态
就绪态,运行态,和阻塞态
多道技术会在进程执行时间过长或遇到IO时自动切换其他进程,意味着IO操作与,进程被剥夺CPU执行权都会造成进程阻塞
六、进程相关理论(了解)
1、进程的创建
但凡是硬件,都需要有操作系统去管理,只要有操作系统,就有进程的概念,就需要有创建进程的方式,一些操作系统只为一个应用程序设计,比如微波炉中的控制器,一旦启动微波炉,进程就已经存在。
而对于通用系统(跑很多应用程序),需要有系统运行过程中创建或撤销进程的能力,主要分为4中形式创建新的进程
-
系统初始化(查看进程linux中用ps命令,windows中用任务管理器,前台进程负责与用户交互,后台运行的进程与用户无关,运行在后台并且只在需要时才唤醒的进程,称为守护进程,如电子邮件、web页面、新闻、打印)
-
一个进程在运行过程中开启了子进程(如nginx开启多进程,os.fork,subprocess.Popen等)
-
用户的交互式请求,而创建一个新进程(如用户双击暴风影音)
-
一个批处理作业的初始化(只在大型机的批处理系统中应用)
无论哪一种,新进程的创建都是由一个已经存在的进程执行了一个用于创建进程的系统调用而创建的:
-
在UNIX中该系统调用是:fork,fork会创建一个与父进程一模一样的副本,二者有相同的存储映像、同样的环境字符串和同样的打开文件(在shell解释器进程中,执行一个命令就会创建一个子进程)
-
在windows中该系统调用是:CreateProcess,CreateProcess既处理进程的创建,也负责把正确的程序装入新进程。
关于创建的子进程,UNIX和windows
1.相同的是:进程创建后,父进程和子进程有各自不同的地址空间(多道技术要求物理层面实现进程之间内存的隔离),任何一个进程的在其地址空间中的修改都不会影响到另外一个进程。
2.不同的是:在UNIX中,子进程的初始地址空间是父进程的一个副本,提示:子进程和父进程是可以有只读的共享内存区的。但是对于windows系统来说,会重新加载程序代码。
2、进程的销毁
-
正常退出(自愿,如用户点击交互式页面的叉号,或程序执行完毕调用发起系统调用正常退出,在linux中用exit,在windows中用ExitProcess)
-
出错退出(自愿,python a.py中a.py不存在)
-
严重错误(非自愿,执行非法指令,如引用不存在的内存,1/0等,可以捕捉异常,try...except...)
-
被其他进程杀死(非自愿,如kill -9)
3、进程的层次结构
无论UNIX还是windows,进程只有一个父进程,不同的是:
-
在UNIX中所有的进程,都是以init进程为根,组成树形结构。父子进程共同组成一个进程组,这样,当从键盘发出一个信号时,该信号被送给当前与键盘相关的进程组中的所有成员。
-
在windows中,没有进程层次的概念,所有的进程都是地位相同的,唯一类似于进程层次的暗示,是在创建进程时,父进程得到一个特别的令牌(称为句柄),该句柄可以用来控制子进程,但是父进程有权把该句柄传给其他子进程,这样就没有层次了。
七、python中实现并发
在一个应用程序中可能会有多个任务需要并发执行,但是对于操作系统而言,一个进程就是一个任务CPU会从上往下依次执行代码,现在我们可以通过交互式方式来启动一个进程,但这并适用于我们的程序,我们不可能要求用户手动来启动进程来玩任务,太low!
方式1:
实例化Process类
from multiprocessing import Process import time def task(name): print('%s is running' %name) time.sleep(3) print('%s is done' %name) if __name__ == '__main__': # 在windows系统之上,开启子进程的操作一定要放到这下面 # Process(target=task,kwargs={'name':'egon'}) p=Process(target=task,args=('jack',)) p.start() # 向操作系统发送请求,操作系统会申请内存空间,然后把父进程的数据拷贝给子进程,作为子进程的初始状态 print('======主')
方式2:
继承Process类 并覆盖run方法
from multiprocessing import Process import time class MyProcess(Process): def __init__(self,name): super(MyProcess,self).__init__() self.name=name def run(self): print('%s is running' %self.name) time.sleep(3) print('%s is done' %self.name) if __name__ == '__main__': p=MyProcess('jack') p.start() print('主')
需要注意的是 在windows下 开启子进程必须放到__main__
下面,因为windows在开启子进程时会重新加载所有的代码造成递归
2、进程间内存相互隔离
from multiprocessing import Process import time x=1000 def task(): global x x=0 print('儿子死啦',x) if __name__ == '__main_ print(x) p=Process(target=task) p.start() time.sleep(5) print(x)
3、join函数
调用start函数后的操作就由操作系统来玩了,至于何时开启进程,进程何时执行,何时结束都与应用程序无关,所以当前进程会继续往下执行,join函数就可以是父进程等待子进程结束后继续执行
jion函数也是有效结局僵尸进程的一个手段
案例1:
from multiprocessing import Process import time x=1000 def task(): time.sleep(3) global x x=0 print('儿子死啦',x) if __name__ == '__main__': p=Process(target=task) p.start() p.join() # 让父亲在原地等 print(x)
案例2:
from multiprocessing import Process import time,random x=1000 def task(n): print('%s is runing' %n) time.sleep(n) if __name__ == '__main__': start_time=time.time() p1=Process(target=task,args=(1,)) p2=Process(target=task,args=(2,)) p3=Process(target=task,args=(3,)) p1.start() p2.start() p3.start() p3.join() #3s p1.join() p2.join() print('主',(time.time() - start_time)) start_time=time.time() p_l=[] for i in range(1,4): p=Process(target=task,args=(i,)) p_l.append(p) p.start() for p in p_l: p.join() print('主',(time.time() - start_time))
4、Process对象常用属性
from multiprocessing import Process def task(n): print('%s is runing' %n) time.sleep(n) if __name__ == '__main__': start_time=time.time() p1=Process(target=task,args=(1,),name='任务1') p1.start() print(p1.pid) print(p1.name) p1.terminate() #终止子进程的命令 p1.join() #如果不加这步,下面一行代码的结果会显示True,这是因为,终止子进程需要花时间,cpu自动运行主进程的代码去了(下一行代码属于主进程) print(p1.is_alive()) print('主')
5、僵尸进程与孤儿进程:
a、僵尸进程: 是指子进程执行完成所有任务后已经终止了,但是还残留一些信息(进程id 进程名) 但是父进程 没有去处理这些残留信息就导致残留信息占用系统内存。 僵尸进程时有害的
解决僵尸进程的方案:
1、结束父进程,这样会使僵尸进程变成孤儿进程,让操作系统接管。
2、在父进程中添加上处理子进程的处理方法,如加上join函数。
b、孤儿进程:是指父进程已经终止了,但是自己还在运行,它是无害的 孤儿进程会自动操作系统接管,因此操作系统会处理器遗留信息。