第五十一篇 并发编程——多进程

时间:2021-08-22 17:58:56

第五十一篇 并发编程——多进程

一、什么是进程

1.进程就是一个程序在一个数据集上的一次动态执行过程

2.进程一般由程序、数据集、进程控制块三部分组成

3.程序:描述进程要完成哪些功能以及如何完成

4.数据集:程序在执行过程中所需要使用的资源

5.进程控制块:记录进程的外部特征,描述进程的执行变化过程。系统可以利用它来控制和管理进程,它是系统感知进程存在的唯一标志

经典举例说明进程,以及切换

1.考虑场景:网易云音乐以及notepad++ 三个软件只能顺序执行是怎样一种场景呢?另外,假如有两个程序A和B,程序A在执行到一半的过程中,需要读取大量的数据输入(I/O操作),而此时CPU只能静静地等待任务A读取完数据才能继续执行,这样就白白浪费了CPU资源。你是不是已经想到在程序A读取数据的过程中,让程序B去执行,当程序A读取完数据之后,让程序B暂停,这里有一个关键词:切换。既然是切换,那么这就涉及到了状态的保存,状态的恢复,加上程序A与程序B所需要的系统资源(内存,硬盘,键盘等等)是不一样的。自然而然的就需要有一个东西去记录程序A和程序B分别需要什么资源,怎样去识别程序A和程序B等等

2.举例说明进程:想象一位有一手好厨艺的计算机科学家正在为他的女儿烘制生日蛋糕。他有做生日蛋糕的食谱,厨房里有所需的原料:面粉、鸡蛋、糖、香草汁等。在这个比喻中,做蛋糕的食谱就是程序(即用适当形式描述的算法)计算机科学家就是处理器(cpu),而做蛋糕的各种原料就是输入数据。进程就是厨师阅读食谱、取来各种原料以及烘制蛋糕等一系列动作的总和。现在假设计算机科学家的儿子哭着跑了进来,说他的头被一只蜜蜂蛰了。计算机科学家就记录下他照着食谱做到哪儿了(保存进程的当前状态),然后拿出一本急救手册,按照其中的指示处理蛰伤。这里,我们看到处理机从一个进程(做蛋糕)切换到另一个高优先级的进程(实施医疗救治),每个进程拥有各自的程序(食谱和急救手册)。当蜜蜂蛰伤处理完之后,这位计算机科学家又回来做蛋糕,从他离开时的那一步继续做下去

二、进程与程序

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

三、线程

1.线程的出现是为了降低上下文切换的消耗,提高系统的并发性,并突破一个进程只能干一件事的缺陷,使得进程内并发成为可能

2.假设,一个文本程序,需要接收键盘输入,将内容显示在屏幕上,还需要保存信息到硬盘中。若只有一个进程,势必造成同一时间只能干一件事的尴尬(当保存时,就不能通过键盘输入内容)。若有多个进程,每个进程负责一个任务,进程A负责接收键盘输入的信息的任务,进程B负责将内容显示在屏幕上的任务,进程C负责保存内容到硬盘中的任务。这里进程A,B,C间的协作涉及到了进程通信的问题,而且有共同都需要拥有的东西--文本内容,不停的切换会造成性能上的损失。若有一种机制,可以使任务A,B,C共享资源,这样上下文切换所需要保存和恢复的内容就少了,同时又可以减少通信所带来的性能损耗,这种机制就是线程

3.线程也叫轻量级进程,它是一个基本的CPU执行单元,也是程序执行过程中的最小单位,由线程ID、程序计数器、寄存器集合和堆栈共同组成。线程的引入减小了程序并发执行时的开销,提高了操作系统的并发性能。线程没有自己的系统资源

进程和线程的关系

1.进程是计算机中的程序关于莫数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位,是操作系统结构的基础。或者说线程是具有一定独立功能的程序关于某个数据集合上的一次运行活动,进程是系统进行资源分配和调动的一个独立单元

2.线程则是进程的一个实体,是CPU调度和分派的基本单元,它是比进行更小的能独立运行的基本单位

3.总结:

  • 1.一个线程只能属于一个进程,而一个进程可以有多个线程,但至少有一个线程
  • 2.资源分配给进程,同一进程的所有线程共享进程的所有资源
  • 3.CPU分给线程,即真正在CPU上运行的是线程

四、进程PID与PPID

1.PID

1.在一个操作系统中通常都会运行多个应用程序,也就是多个进程,系统会给每一个进程分配一个进程编号,即PID,如同身份证

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

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

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

2.PPID

1.当一个进程a开启了另一个进程b时,a称为b的父进程,b称为a的子进程

2.在python中可以通过os模块来获取父进程的pid

# 在python中可以通过使用os模块来获取ppid
import os
print(os.getpid())  # 获取当前进程自己的Pid
print(os.getppid())  # 获取当前进程的父进程的pid

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

1.并行处理(Parallel Processing)是计算机系统中能同时执行两个或更多个处理的一种计算方法。并行处理可以同时工作于同一程序的不同方面。并行处理的主要目的是节省大型和复杂问题的解决时间

2.并发处理(Concurrency Processing):指一个时间段中有几个程序都处于已启动运行到运行完毕之间,且这几个程序都是在同一个处理器上运行,但任一个时间点上只有一个程序在CPU上运行

3.并发的关键:拥有处理多个任务的能力,不一定要同时

4.并行的关键:拥有同时处理多个任务的能力。所以说,并行是并发的子集

第五十一篇 并发编程——多进程

阻塞与非阻塞

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

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

3.非阻塞表示程序正在正常被CPU执行

4.进程有三种状态:就绪态、运行态和阻塞态

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

同步和异步

在计算机领域,同步就是指一个进程在执行某个请求的时候,若该请求需要一段时间才能返回信息,那么这个进程将会一直等待下去,直到收到返回信息才继续执行下去;异步是指进程不需要一直等下去,而是继续执行下面的操作,不管其他进程的状态。当有消息返回时系统会通知进程进行处理,这样可以提高执行的效率。举个例子,打电话时就是同步通信,发短息时就是异步通信

六、python实现多进程

os.fork()

# 1.只用于Unix系统,windows系统中无效
# 2.fork函数调用一次,返回两次:在父进程中返回值为子进程id,在子进程中返回值为0

import os
pid = os.fork()
if pid == 0:
    print('执行子进程,子进程pid = {pid},父进程ppid = {pidd}'.format(pid = os.getpid(), ppid = os.getppid()))
else:
    print('执行父进程,子进程pid = {pid},父进程ppid = {pidd}'.format(pid = pid, ppid = os.getpid()))

multiprocessing模块,创建Process的实例

# 使用multiprocessing模块: 创建Process的实例,传入任务执行函数作为参数
'''
Process常用属性与方法:
    name:进程名
    pid:进程id
    run(),自定义子类时复写(覆写)
    start(),开启进程
    join(timeout=None),阻塞进程
    terminate(),终止进程
    is_alive(),判断进程是否存活
'''
import os,time
from multiprocessing import Process
def worker():
    print('子进程执行中>>>pid={0},ppid={1}'.format(os.getpid(),os.getppid()))
    time.sleep(2)
    print('子进程终止>>>pid={0}'.format(os.getpid()))
    
def main():
    print('主进程执行中>>>pid={0}'.format(os.getpid()))
    ps = []
    
    for i in range(2):
        p = Process(target=worker,name='worker'+str(i),args=())
        ps.append(p)
    
    for i in range(2):
        ps[i].start()
        
    for i in range(2):
        ps[i].join()
    print('主进程终止')
    
if __name__ == '__main__':
    main()
import os
from multiprocessing import Process

def task():
    print(f'子进程id:{os.getpid()}')

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

    print(f'父进程id:{os.getpid()}')

特别注意:

  • 1.在Windows下开启子进程必须放到__main__下面,因为Windows在开启子进程时会重新加载所有的代码造成递归创建进程,在pycharm中直接会报错
  • 2.start仅仅是给操作系统发送消息,而操作系统创建进程所花费的时间是不确定的,所以是先打印父进程中的消息还是子进程的消息,取决于操作系统创建进程以及开启进程的速度

multiprocessing模块,派生Process的子类

import os
from multiprocessing import Process

class MyProcess(Process):
    # 可写可不写
    # def __init__(self):
        # Process.__init__(self)
    
    def run(self):
        print(f'子进程:{os.getpid()}')

if __name__ == '__main__':
    p = MyProcess()
    p.start()
    # p.run()  # 在当前进程中执行run方法,不属于当前进程的子进程
    print(f'父进程:{os.getpid()}')

1.必须将需要执行的代码放到run方法中,子进程只会执行run方法

2.如果直接使用run方法,而不是使用start,则run是在父进程的id(pid)

# 使用multiprocessing模块: 派生Process的子类,重写run方法
import os, time
from multiprocessing import Process
class MyProcess(Process):
    def __init__(self):
        Process.__init__(self)
    
    def run(self):
        print('子进程开始>>>pid={0},ppid={1}'.format(os.getpid(), os.getppid()))
        time.sleep(2)
        print('子进程终止>>>pid={}'.format(os.getpid()))
        
def main():
    print('主进程开始>>>pid={}'.format(os.getpid()))
    myp = MyProcess()
    myp.start()
    # myp.join()
    print('主进程终止')
        
if __name__ == '__mian__':
    main()

使用进程池Pool

import os, time
from multiprocessing import Pool
def worker(arg):
    print('子进程开始执行>>>pid={}, ppid={}, 编号{}'.format(os.getpid(), os.getppid(), arg))
    time.sleep(0.5)
    print('子进程终止>>>pid={}, ppid={}, 编号{}'.format(os.getpid(),os.getppid(), arg))
    
def main():
    print('主进程开始执行>>>pid={}'.format(os.getpid()))
    ps = Pool(5)
    for i in range(10):
        # ps.apply(worker, args = (i,))  # 同步执行
        ps.apply_async(worker, args=(i,))  # 异步执行
    
    # 关闭进程池,停止接受其它进程
    ps.close()
    # 阻塞进程
    ps.join()
    print('主进程终止')
if __name__ == '__main__':
    main()

join函数

改变当前进程的优先级,一般用于让主进程等待子进程执行完毕后再运行

from multiprocessing import Process
import time
def task1(name):
    for i in range(10000):
        print("%s run" % name)

def task2(name):
    for i in range(100):
        print("%s run" % name)

if __name__ == '__main__': # args 是给子进程传递的参数 必须是元组
    p1 = Process(target=task1,args=("p1",))
    p1.start()  # 向操作系统发送指令
    # p1.join()   # 让主进程 等待子进程执行完毕在继续执行

    p2 = Process(target=task2,args=("p2",))
    p2.start()  # 向操作系统发送指令

    p2.join()  # 让主进程 等待子进程执行完毕在继续执行
    p1.join()

    #需要达到的效果是 必须保证两个子进程是并发执行的 并且 over一定是在所有任务执行完毕后执行
    print("over")

通过join函数可以让进程对象优先执行

# join的使用案例

from multiprocessing import Process
import time
def task1(name):
    for i in range(10):
        print("%s run" % name)

if __name__ == '__main__': # args 是给子进程传递的参数 必须是元组

    ps = []
    for i in range(10):
        p = Process(target=task1,args=(i,))
        p.start()
        ps.append(p)

    # 挨个join以下
    for i in ps:
        i.join()

    print("over")

七、孤儿进程与僵尸进程

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

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

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

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

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

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

4.python已经帮我们自动处理了僵尸进程的回收工作

八、总结:

1.并发编程

1.为什么要并发

  • 因为之前执行程序都是串行的,效率低,需要一种能同时执行多个任务的方法,来提高程序的效率

2.如何实现并发

  • 1.多进程(核心原理是多道技术)
  • 2.多线程
  • 3.协程

2.多道技术

  • 1.空间复用:物理?隔离内存空间
  • 2.时间复用:快速切换 + 保存进度
  • 注意:切换的时机:1.遇到IO;2.或是执行间隔超出阈值

3.并发重要概念

  • 1.串行:代码自上而下顺序执行
  • 2.并发:在不同任务之间快速切换
  • 3.并行:真正的同时执行多个任务,需要多核cup
  • 这些指的是:处理任务(进程)的方式

4.程序的状态

  • 1.阻塞:一般是由于遇到IO操作而引起的
  • 2.非阻塞:没有遇到IO操作

5.进程的三种状态

  • 1.阻塞
  • 2.运行
  • 3.就绪

6.使用多进程的两种方法

  • 1.导入multiprocessing中的Process类,通过实例化这个类,用参数target来指定要执行的任务
  • 注意:
    • 1.linux 与windows开启进程的方式不同,linux 会将父进程的内存数据 完整copy一份给子进程
    • windows 会导入父进程的代码 从头执行一遍 来获取需要处理的任务,所以在编写代码时如果是windows一定要将开启进程的代码放main判断中,linux 可以不放
  • 2.导入multiprocessing中的Process类,继承这个类,覆盖run方法,将要执行的任务放入run中,开启进程时会自动执行该函数

7.join函数

让主进程等待子进程执行完毕后,再继续执行主进程

8.进程对象的常用属性

  • 1.对象名.daemon : 守护进程
  • 2.对象名.exitcode: 获取进程的退出码
  • 3.对象名.is_alive(): 查看进程是否存活
  • 4.对象名.pid : 获取进程id
  • 5.对象名.terminate(): 终止进程

9.PID 和 PPID

  • 1.PID是当前进程的编号
  • 2.PPID是父进程的编号
  • 3注意:当我们运行py文件时,其实运行的是python解释器,所以当期文件的PID是在python中的
  • 4通过 import os ,利用 os.getpid()访问当前进程的编号,os.getppid()访问当前进程的父进程的编号

10.进程和程序

  • 1.程序是一堆代码放在一个文件中,通常后缀是exe,存储在硬盘中
  • 2.进程是将代码从硬盘读取到内存,然后执行产生的
  • 3.因此可以说,进程是由程序产生的
  • 4.一个程序可以产生多个进程,例如QQ可以开启多个
  • 5.每个进程都具备一个PID进程编号,且该编号是唯一的,也是可变的

11.进程的创建和销毁

对于通用计算机(就是电脑,不像洗衣机只能洗衣服)而已,必须具备创建和销毁进程的能力

  • 1.创建

    1.用户的交互式请求(比如鼠标双击图标)
    2.由一个正在运行的程序调用了开启进程的接口(比如subprocess,在程序中执行shell命令)
    3.批处理
    4.系统初始化(自动创建进程,比如开启防火墙)
  • 2.销毁

    1.任务完成,自愿退出
    2.强制结束(taskkill,非自愿。Linux中是kill)
    3.程序遇到异常
    4.严重错误(比如访问了不该访问的内存)