Cpython 解释器下实现并发编程

时间:2021-02-22 15:56:42

背景知识:

顾明思议: 进程即正在执行的一个过程,进程是对正在运行程序的一个抽象。

进程的概念起源于操作系统, 是操作系统最核心的概念,也是操作系统提供的最古老的最重要的抽象概念之一。操作系统的其他所有被人都是围绕进程的概念展开的。

所以要想真正了解进程, 必须要事先了解操作系统必备的理论基础。

一:操作系统的作用:

1、隐藏丑陋复杂的硬件接口, 提供良好的抽象接口, 

2、管理调度进程, 并且将多个进程对硬件的竞争变得有序

 

二、多道技术:

1、产生的北京:针对单核,实现并发

ps:

现在的主机一般是多核, 那么每个核都会利用多道技术

有4个cpu, 运行Cpu1 de 某个程序遇到IO阻塞,会等到io结束在重新调度, 会被调度到4个cpu中的任意一个, 具体由操作系统调度算法决定

2、空间上的复用:内存总同时又多道程序

3、时间上的复用:复用一个cpu 的时间片,

  强调:遇到io切,占用cpu 时间过长也切, 核心在于切之前将进程的状态保存下来,这样才能保证下次切换回来时, 能基于上次切走的位置继续运行

 

三、 什么是进程

进程:正在进行的一个过程或者说一个任务, 而负责执行任务则是CPU

进程与程序的区别:程序仅仅只是一堆代码, 而进程是指程序的运行的过程。

需要强调的是:同一个程序执行两次,那也是两个进程,比如打开腾讯视频, 虽然都是同一个软件, 但是一个可以看还珠格格 ,一个可以播放熊出没。

四、并发与并行

无论是并行还是并发,在用户看来都是同时运行的,不管是进程还是线程, 都只是一个任务而已,真正干活的是cpu,cpu 来做这些任务, 而一个cpu同一时刻只能执行一个任务

一 并发: 是伪并行, 即看起来是同时运行,单个CPU +多道技术就可以实现并发(并行也是并发)

二 并行: 同时运行,只有具备多个CPU才能实现并行

单核下的话:可以利用多道技术,

多个核的话:每个核也可都可以利用多道技术(多道技术是针对单核而言)

例如:

有四个核,六个任务,这样同一时间有四个任务呗执行, 假设分别被分配给了 cpu1,cpu2,cpu3,cpu4

一旦任务1遇到i/o 就*中断执行, 此时任务5 就拿到CPU1 的时间片区执行, 这就是单核下的多道技术。

而一旦任务1 的I/O结束了, 操作系统会重新调用它(需知进程的调度, 分配给哪个cpu运行,由操作系统说了算,)可能被分配给四个cpu中的任意一个取执行

 

所有现代计算机经常会在同一时间做很多件事,一个用户的PC(无论是单cpu还是多cpu),都可以同时运行多个任务(一个任务可以理解为一个进程)。

    启动一个进程来杀毒(360软件)

    启动一个进程来看电影(暴风影音)

    启动一个进程来聊天(腾讯QQ)

所有的这些进程都需被管理,于是一个支持多进程的多道程序系统是至关重要的

多道技术概念回顾:内存中同时存入多道(多个)程序,cpu从一个进程快速切换到另外一个,使每个进程各自运行几十或几百毫秒,这样,虽然在某一个瞬间,一个cpu只能执行一个任务,但在1秒内,cpu却可以运行多个进程,这就给人产生了并行的错觉,即伪并发,以此来区分多处理器操作系统的真正硬件并行(多个cpu共享同一个物理内存)

 

五:进程的创建

 但凡是硬件,都需要有操作系统去管理,只要有操作系统,就有进程的概念,就需要有创建进程的方式,一些操作系统只为一个应用程序设计,比如微波炉中的控制器,一旦启动微波炉,所有的进程都已经存在。

  而对于通用系统(跑很多应用程序),需要有系统运行过程中创建或撤销进程的能力,主要分为4中形式创建新的进程

  1. 系统初始化(查看进程linux中用ps命令,windows中用任务管理器,前台进程负责与用户交互,后台运行的进程与用户无关,运行在后台并且只在需要时才唤醒的进程,称为守护进程,如电子邮件、web页面、新闻、打印)

  2. 一个进程在运行过程中开启了子进程(如nginx开启多进程,os.fork,subprocess.Popen等)

  3. 用户的交互式请求,而创建一个新进程(如用户双击暴风影音)

  4. 一个批处理作业的初始化(只在大型机的批处理系统中应用)

  

  无论哪一种,新进程的创建都是由一个已经存在的进程执行了一个用于创建进程的系统调用而创建的:

1、在UNIX 中该系统调用的是fork ,fork 会创建一个与父进程一模一样的副本, 二者有相同的存储映像,同样的环境字符串和同样的打开文件

2、在windows中该系统调用的是CreatProcess,CreatProcess 既处理进程的创建, 也赋值把正确的程序装入新进程。

 

 

关于子进程的创建, UNIX 和windows :

1:相同的是:进程创建后, 父进程和子进程有个字不通的地址空间(多道技术要求物理层面实现进程之间内存的隔离)任何一个进程的在其地址空间中的修改都不会影响到另外一个进程。

 

2:不同的是:在UNIX 中,子进程的初始地址空间是父进程的一个副本,提示:子进程和父进程是可以有只读的共享内存区的。但是对于windows 系统来说, 从一开始父进程与子进程的地址空间就是不同的

windows 系统创建子进程。需要导入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__':
    #在window 系统之上,开启子进程的操作一定要放到这下面
    p=Process(target=task,args=('egon',))       #等同于==》p=Process(target=task,kwargs={'name':'egon'})
    p.start()                               #父进程向操作系统发送请求,操作系统会申请内存空间, 然后把父进程的数据拷贝给子进程, 作为子进程的初始状态
    print('====主')                         #为父进程操作, 对比子进程结束与父进程的先后运行


#p.start()父进程向操作系统发送开启子进程请求后, 剩下的都有操作系统与CPU操作,无需父进程在来操作,
父进程会继续执行申请开启子进程后 下面的代码:print(‘====主’)。 而操作系统和CPU来控制运行子进程时时间没有那么快 ,要比父进程运行print(‘====主’)慢

所以时打印结果

====主
egon is running

#睡三秒后运行下面一行
egon is done

 

另外一种创建模式  是用类写

from multiprocessing  import  Process  #process  是个别人已经写好的类 ,我们可以直接用

import   time

class MyProcess(Process):   #自己创建一个类, 将Process 当做父类
    def __init__(self,name):
        super(MyProcess,self).__init__()
        self.name =name

    def run(self):       #当运行p.start() 时直接走这一步 run(),
        print('%s is running' %self.name)
        time.sleep(3)
        print('%s is done'%self.name)


if __name__ =='__main__':
    p=MyProcess('egon')#传参
    p.start()
    print('')

    # 所以这个打印结果
    主
    # egon is running
    # egon is done

六 进程的内存空间是相互隔离的

以下面的例子来验证

from multiprocessing import Process
import time

x=1000  # 父进程变量

def task():
    time.sleep(3)
    global x  # 初始值是x=1000, 然后在子进程的内存空间中中修改 x 的值
    x=0
    print('儿子死啦',x) #这里的x 是子进程里的代码    x=0


if __name__ == '__main__':
    print(x) #这个也是父进程代码  x=1000
    p=Process(target=task)
    p.start()
    time.sleep(5)  #睡了5秒,这时子进程已经执行完毕
    print(x) # 这也是父类代码 所以x=1000


#所以打印效果如下
1000
儿子死啦 0
1000

七:父进程等待子进程结束  用join():

1,少进程版的:

from multiprocessing import Process
import time

x=1000  # 父进程变量

def task():
    time.sleep(3)
    global x
    x=0
    print('儿子死啦',x)


if __name__ == '__main__':
    print(x)
    p=Process(target=task)
    p.start()

    
    p.join()  # 让父亲在原地等待子进程结束 在进行下面的代码
    print(x)

#打印结果:

1000
儿子死啦 0
1000

 

2、多进程

lower版:

from multiprocessing import Process
import time

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() 
    p1.join()
    p2.join()
    
    print('',(time.time()-start_time))
#打印借结果

1 is runing
2 is runing
3 is runing
主 3.1258764266967773

 

 

循环版:

from multiprocessing import Process
import time

x=1000 # 父进程变量

def task(n):
print('%s is runing '%n)
time.sleep(n)


if __name__ == '__main__':
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))

#打印结果

1 is runing
2 is runing
3 is runing
主 3.124988079071045

进程的一些属性介绍

1、process 类的介绍

创建进程的类

Process([group [, target [, name [, args [, kwargs]]]]]),由该类实例化得到的对象,表示一个子进程中的任务(尚未启动)

强调:
1. 需要使用关键字的方式来指定参数
2. args指定的为传给target函数的位置参数,是一个元组形式,必须有逗号

参数的介绍

1 group参数未使用,值始终为None

3 target表示调用对象,即子进程要执行的任务

5 args表示调用对象的位置参数元组,args=(1,2,'egon',)

7 kwargs表示调用对象的字典,kwargs={'name':'egon','age':18}

9 name为子进程的名称

方法介绍

1 p.start():启动进程,并调用该子进程中的p.run() 
 2 p.run():进程启动时运行的方法,正是它去调用target指定的函数,我们自定义类的类中一定要实现该方法  
 3 
 4 p.terminate():强制终止进程p,不会进行任何清理操作,如果p创建了子进程,该子进程就成了僵尸进程,使用该方法需要特别小心这种情况。如果p还保存了一个锁那么也将不会被释放,进而导致死锁
 5 p.is_alive():如果p仍然运行,返回True
 6 
 7 p.join([timeout]):主线程等待p终止(强调:是主线程处于等的状态,而p是处于运行的状态)。timeout是可选的超时时间,需要强调的是,p.join只能join住start开启的进程,而不能join住run开启的进程  

属性介绍

1 p.start():启动进程,并调用该子进程中的p.run() 
 2 p.run():进程启动时运行的方法,正是它去调用target指定的函数,我们自定义类的类中一定要实现该方法  
 3 
 4 p.terminate():强制终止进程p,不会进行任何清理操作,如果p创建了子进程,该子进程就成了僵尸进程,使用该方法需要特别小心这种情况。如果p还保存了一个锁那么也将不会被释放,进而导致死锁
 5 p.is_alive():如果p仍然运行,返回True
 6 
 7 p.join([timeout]):主线程等待p终止(强调:是主线程处于等的状态,而p是处于运行的状态)。timeout是可选的超时时间,需要强调的是,p.join只能join住start开启的进程,而不能join住run开启的进程  

 

八:僵尸进程与孤儿进程

一:僵尸进程(有害)
  僵尸进程:一个进程使用fork创建子进程,如果子进程退出,而父进程并没有调用wait或waitpid获取子进程的状态信息,那么子进程的进程描述符仍然保存在系统中。这种进程称之为僵死进程。
   直到父进程通过wait / waitpid来取时才释放. 但这样就导致了问题,如果进程不调用wait / waitpid的话,那么保留的那段信息就不会释放,其进程号就会一直被占用,但是系统所能使用的进程号是有限的,如果大量的产生僵死进程,将因为没有可用的进程号而导致系统不能产生新的进程. 此即为僵尸进程的危害,应当避免。

 

二:孤儿进程(无害)

  孤儿进程:一个父进程退出,而它的一个或多个子进程还在运行,那么那些子进程将成为孤儿进程。孤儿进程将被init进程(进程号为1)所收养,并由init进程对它们完成状态收集工作。

  孤儿进程是没有父进程的进程,孤儿进程这个重任就落到了init进程身上,init进程就好像是一个民政局,专门负责处理孤儿进程的善后工作。每当出现一个孤儿进程的时候,内核就把孤 儿进程的父进程设置为init,而init进程会循环地wait()它的已经退出的子进程。这样,当一个孤儿进程凄凉地结束了其生命周期的时候,init进程就会代表党和*出面处理它的一切善后工作。因此孤儿进程并不会有什么危害。