多进程并发编程

多进程实现原理-多道技术

操作系统介绍

下图是操作系统在整个计算机中所在的位置:

image-20181228060338147

位于应用软件和硬件设备之间,本质上也是一个软件,

由系统内核(管理所有硬件资源)与系统接口(提供给程序员使用的接口)组成

操作系统是为方便用户操作计算机而提供的一个运行在硬件之上的软件

操作系统的两个核心作用

1.为用户屏蔽了复杂繁琐的硬件接口,为应用程序提供了,清晰易用的系统接口

有了这些接口以后程序员不用再直接与硬件打交道了

例子:有了操作系统后我们就可以使用资源管理器来操作硬盘上的数据,而不用操心,磁头的移动啊,数据的读写等等

2.操作系统将应用程序对硬件资源的竞争变成有序的使用

例子:所有软件 qq啊 微信啊 吃鸡啊都共用一套硬件设备 假设现有三个程序都在使用打印机,如果不能妥善管理竞争问题,可能一个程序打印了一半图片后,另一个程序抢到了打印机执行权于是打印了一半文本,导致两个程序的任务都没能完成,操作系统的任务就是将这些无序的操作变得有序

操作系统与应用程序的区别

它们都是软件,而操作系统可以看做一款特殊的软件

1.操作系统是是受保护的:无法被用户修改(应用软件如qq不属于操作系统可以随便卸载)

2.大型:linux或widows源代码都在五百万行以上,这仅仅是内核,不包括用户程序,如GUI,库以及基本应用软件(如windows Explorer等),很容易就能达到这个数量的10倍或者20倍之多

3.长寿:由于操作系统源码量巨大,编写是非常耗时耗力的,一旦完成,操作系统所有者便不会轻易的放弃重写,二是在原有基础上改进,基本上可以把windows95/98/Me看出一个操作系统

多道技术

多道技术中的多道指的是多个程序,多道技术的实现是为了解决多个程序竞争或者说共享同一个资源(比如cpu)的有序调度问题,解决方式即多路复用,多路复用分为时间上的复用和空间上的复用。

空间复用

同一时间,加载多个任务到内存中,多个进程之间内存区域需要相互隔离,这种隔离是物理层面的隔离,其目的是为了保证数据安全

时间复用

指的是,操作系统会在多个进程之间做切换执行

​ 切换任务的两种情况 :

​ 1.当一个进程遇到了IO操作,时会自动切换

​ 2.当一个任务执行时间超过阈值,会强制切换

​ 注意:在切换前必须保存状态,以便后续恢复执行

频繁的切换其实也需要消耗资源,当所有任务都没有IO操作时,切换执行效率反而降低,但是为了保证并发执行,必须牺牲效率

并发编程-多进程

什么是并发编程

并发指的是多个任务同时被执行,并发编程指的是编写支持多任务并发的应用程序在。

之前的TCP通讯中,服务器在建立连接后需要一个循环来与客户端循环的收发数据,但服务器并不知道客户端什么时候会发来数据,导致没有数时服务器进入了一个等待状态,此时其他客户端也无法链接服务器,很明显这是不合理的,学习并发编程就是要找到一种方案,让一个程序中的的多个任务可以同时被处理;

什么是进程

进程指的是正在运行的程序,是一系列过程的统称,也是操作系统在调度和进行资源分配的基本单位

进程是实现并发的一种方式,在学习并发编程之前要先了解进程的基本概念以及多进程的实现原理,这就不得不提到操作系统了,因为进程这个概念来自于操作系统,没有操作系统就没有进程

多进程的实现原理见:
https://www.cnblogs.com/yangyuanhu/p/11112763.html

进程与程序

进程是正在运行的程序,程序是程序员编写的一堆代码,也就是一堆字符,当这堆代码被系统加载到内存中并执行时,就有了进程。

例如:生活中我们会按照菜谱来做菜,那么菜谱就是程序,做菜的过程就是进程

需要注意的是:一个程序是可以产生多个进程的,就像我们可以同时运行多个QQ程序一样,会形成多个进程

测试:

import time
while True:
    time.sleep(1)

多次运行该文件,就会产生多个python.exe进程,可以通过tasklist来查看运行的程序

PID和PPID

PID

在一个操作系统中通常都会运行多个应用程序,也就是多个进程,那么如何来区分进程呢?

系统会给每一个进程分配一个进程编号即PID,如同人需要一个身份证号来区分。

验证:

tasklist 用于查看所有的进程信息

taskkill /f /pid pid 该命令可以用于结束指定进程

# 在python中可以使用os模块来获取pid
import os
print(os.getpid())

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的父进程

并发与并行,阻塞与非阻塞

并发指的是,多个事件同时发生了

例如洗衣服和做饭,同时发生了,但本质上是两个任务在切换,给人的感觉是同时在进行,也被称为伪并行

并行指的是,多个事件同时进行着

例如一个人在写代码另一个人在写书,这两件事件是同时在进行的,要注意的是一个人是无法真正的并行执行任务的,在计算机中单核CPU也是无法真正并行的,之所以单核CPU也能同时运行qq和微信其实就是并发执行

阻塞与非阻塞指的是程序的状态

阻塞状态是因为程序遇到了IO操作,或是sleep,导致后续的代码不能被CPU执行

非阻塞与之相反,表示程序正在正常被CPU执行

补充:进程有三种状态

就绪态,运行态,和阻塞态

img

多道技术会在进程执行时间过长或遇到IO时自动切换其他进程,意味着IO操作与进程被剥夺CPU执行权都会造成进程无法继续执行

python中实现多进程

在一个应用程序中可能会有多个任务需要并发执行,但是对于操作系统而言,一个进程就是一个任务,CPU会从上往下依次执行代码,当代码中遇到IO操作时,操作系统就会剥夺CPU执行权给其他应用程序,这样对于当前应用程序而言,效率就降低了,如何使得程序既能完成任务又不降低效率呢?答案就是让把当前程序中的耗时操作交給子进程来完成,如此当前应用程序可以继续执行其他任务!

python中开启子进程的两种方式

方式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('主')

需要注意的是

1.在windows下 开启子进程必须放到__main__下面,因为windows在开启子进程时会重新加载所有的代码造成递归创建进程

2.第二种方式中,必须将要执行的代码放到run方法中,子进程只会执行run方法其他的一概不管

3.start仅仅是给操作系统发送消息,而操作系统创建进程是要花费时间的,所以会有两种情况发送

3.1开启进程速度慢于程序执行速度,先打印”主“ 在打印task中的消息

3.2开启进程速度快于程序执行速度,先打印task中的消息,在打印”主“

进程间内存相互隔离

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)

join函数

调用start函数后的操作就由操作系统来玩了,至于何时开启进程,进程何时执行,何时结束都与应用程序无关,所以当前进程会继续往下执行,join函数就可以是父进程等待子进程结束后继续执行

案例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))

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) # 获取进程pid
    print(p1.name) # 获取进程名字
    p1.terminate() # 终止进程
    p1.join() # 提高优先级
    print(p1.is_alive()) # 获取进程的存活状态
    print('主')

孤儿进程与僵尸进程

什么是孤儿进程

孤儿进程指的是开启子进程后,父进程先于子进程终止了,那这个子进程就称之为孤儿进程

例如:qq聊天中别人发给你一个链接,点击后打开了浏览器,那qq就是浏览器的父进程,然后退出qq,此时浏览器就成了孤儿进程

孤儿进程是无害的,有其存在的必要性,在父进程结束后,其子进程会被操作系统接管。

什么是僵尸进程

僵尸进程指的是,当子进程比父进程先结束,而父进程又没有回收子进程,释放子进程占用的资源,此时子进程将成为一个僵尸进程。该情况仅在linux下出现。windows中进程间完全是独立的没有任何关联。

如果父进程先退出 ,子进程被操作系统接管,子进程退出后操作系统会回收其占用的相关资源!

僵尸进程的危害

由于子进程的结束和父进程的运行是一个异步过程,即父进程永远无法预测子进程 到底什么时候结束. 那么会不会因为父进程太忙来不及wait子进程,或者说不知道 子进程什么时候结束,而丢失子进程结束时的状态信息呢? 不会。因为UNⅨ提供了一种机制可以保证只要父进程想知道子进程结束时的状态信息, 就必然可以得到。这种机制就是: 在每个进程退出的时候,内核释放该进程所有的资源,包括打开的文件,占用的内存等。但是仍然为其保留一定的信息(包括进程号the process ID,退出状态the termination status of the process,运行时间the amount of CPU time taken by the process等)。直到父进程通过wait / waitpid来取时才释放. 但这样就导致了问题,如果进程不调用wait / waitpid的话,那么保留的那段信息就不会释放,其进程号就会一直被占用,但是系统所能使用的进程号是有限的,如果大量的产生[僵死进程],将因为没有可用的进程号而导致系统不能产生新的进程. 此为僵尸进程的危害,应当避免。

原文地址:https://www.cnblogs.com/gaohuayan/p/11123136.html