Python并发编程之进程操作
前言
本文介绍一下如何在python中开启进程。
查看进程
一台计算机上面运行着很多进程,计算是如何区分并管理这些进程服务端的呢:计算机会给每一个运行的程序即进程分配一个PID号。
windows系统电脑进入cmd后输入tasklist即可查看所有进程的进程号;
mac或者linux系统电脑进入终端输入ps aux查看进程。
如何在代码中查看当前进程的PID呢?又如何在python代码中开启进程呢?可以借助multiprocessing模块。
multiprocessing模块
python提供了multiprocessing模块,该模块可以用来开启子进程、查看当前进程号等,提供了Process、Queue、Pipe、Lock等组件。
开启子进程 - Process类的使用
需要注意的是,在windows操作系统下,开启进程必须在if__name__ == '__main__' 的分支下,原文如下:
Since Windows has no fork, the multiprocessing module starts a new Python process and imports the calling module. If Process() gets called upon import, then this sets off an infinite succession of new processes (or until your machine runs out of resources). This is the reason for hiding calls to Process() inside.
在Python中创建和开启进程可以使用Process类,有两种方式:
方式一:经常使用的方式
import time import os from multiprocessing import Process, current_process ''' current_process().pid可以获取当前进程的进程号PID os.getpid()也可以获取当前进程的进程号PID ''' # 子进程的任务 def func(name): print(f'{name} start') time.sleep(2) print(f'func {current_process().pid},current——process获取进程号') print(f'func {os.getpid()},OS获取进程号') print(f'{name} end') if __name__ == '__main__': # 创建子进程,target是子进程需要执行的任务,args是任务的参数,为元组形式 p = Process(target=func, args=('zhuang', )) # 开启子进程 p.start() # 执行当前文件就是主进程 print(f'current_process获取进程号{current_process().pid},主进程结束') print(f'os获取进程号{os.getpid()},主进程结束') # 代码运行结果 current_process获取进程号2868,主进程结束 os获取进程号2868,主进程结束 zhuang start func 26520,current——process获取进程号 func 26520,OS获取进程号 zhuang end
方式二:继承Process类,重写父类的run方法
import time import os from multiprocessing import Process, current_process # 子进程的任务 def func(name): print(f'{name} start') time.sleep(2) print(f'func {current_process().pid},current——process获取进程号') print(f'func {os.getpid()},OS获取进程号') print(f'{name} end') class MyProcess(Process): def __init__(self, name): super().__init__() self.name = name def run(self): # start会自动调用run func(self.name) if __name__ == '__main__': p = MyProcess('zhuang') p.start() # p.run()只有通过start开启的才是子进程,通过run开启的不是子进程 print('主进程结束')
通过上述代码的执行结果,我们可以发现,主进程并没有等子进程执行完毕,这就是一个异步的操作。我们可以同时开多个子进程:
from multiprocessing import Process def func(name): print(f'{name} is start') time.sleep(2) print(f'{name} is done') if __name__ == '__main__': for i in range(1, 4): p = Process(target=func, args=(f'p{i}', i)) p.start() print('主进程结束') # 输出: 主进程结束 p3 is start p1 is start p2 is start p1 is done p2 is done p3 is done
同时开启三个子进程,打印的顺序也就是执行顺序是变化的,子进程通过start开启,本质是应用程序向操作系统发起请求,让操作系统开子进程。这是一个异步的操作,操作系统接到任务后,会根据整体情况做统筹安排,所以每个子进程开启的时间不按顺序,结束的时间也不按顺序。并且主进程并没有等待子进程结束才运行。
join方法
上述代码中主进程没有等待子进程结束就先运行了,而join方法就可以让主进程等待子进程结束后才运行主进程。如下述代码:
import time import os from multiprocessing import Process # 子进程的任务 def func(name): print(f'{name} is start') time.sleep(2) print(f'{name} is end') if __name__ == '__main__': # 创建子进程,target是子进程需要执行的任务,args是任务的参数,为元组形式 p = Process(target=func, args=('zhuang', )) # 开启子进程 p.start() # 等待子进程结束才往下运行 p.join() # 执行当前文件就是主进程 print(f'os获取进程号{os.getpid()},主进程结束') # 运行结果如下 zhuang start zhuang end os获取进程号15816,主进程结束
主进程等待子进程的过程也分为两种情况:
第一种:先开启所有子进程再让主进程等待
当主进程等待子进程的时候,卡住的是主进程。此时所有的子进程已经交给操作系统开启了,子进程会正常运行的,等所有的子进程运行结束,主进程再运行。此时每个子进程是并发运行的,因此主进程等待的时间是子进程所需运行时间最长的3s左右,如下代码
import time import os from multiprocessing import Process # 子进程的任务 def func(name, i): print(f'{name} start') time.sleep(i) print(f'{name} end') if __name__ == '__main__': p_list = [] for i in range(1, 4): p = Process(target=func, args=(f'p{i}', i)) p_list.append(p) p.start() # 先开启所有子进程 for p in p_list: p.join() # 再让主进程等待子进程 print('主进程结束') # 运行结果如下 p2 is start p1 is start p3 is start p1 is done p2 is done p3 is done 主进程结束
第二种,每个子进程开启后都需要执行完成后才能往下执行
这种情况程序的运行就会变成串行,每个进程都需要等上个进程执行完毕才能执行。如下代码
import time import os from multiprocessing import Process # 子进程的任务 def func(name, i): print(f'{name} start') time.sleep(i) print(f'{name} end') if __name__ == '__main__': for i in range(1, 4): p = Process(target=func, args=(f'p{i}', i)) p.start() # 先开启所有子进程 p.join() # 一个一个运行子进程 print('主进程结束') # 运行结果,变成了串行 p1 is start p1 is done p2 is start p2 is done p3 is start p3 is done 主进程结束
进程之间内存隔离
进程之间内存空间相互隔离,即进程之间数据相互隔离,互不影响。不同进程有不同的名称空间,只修改某一个进程名称空间中名字的值,不影响其他进程名称空间的名字的值。如下述代码:
money = 100 def task(): global money money = 666 print('子', money) # 666 if __name__ == '__main__': p = Process(target=task) p.start() p.join() # 主进程等待子进程p结束后再执行 print('主', money) # 100
进程对象的属性
进程对象是Process类的一个对象,或者是继承了Process类实例化得到的对象,这个进程对象有很多数据属性和方法供我们使用。
from multiprocessing import Process, current_process import os # 进程对象的数据属性和函数属性 p.start() # 开启子进程 p.join() # 主进程等待p进程结束 p.run() # p进程具体执行的任务,可以重写这个方法 p.is_alive() # 判断子进程是否存活,返回布尔值 p.terminate() # 结束子进程, 异步调用操作系统,所以不会立马关闭 p.kill() # 结束子进程,同理 p.name # 获取子进程的名字或修改之,注意自定义类时name属性的命名冲突问题,先super再赋值 p.daemon # 判断子进程是否为守护进程,或者设置其为守护进程,p.daemon = True # 补充:获取进程的pid current_process().pid # 当前进程pid os.getpid() # 当前进程pid os.getppid() # 当前进程的父进程的pid
僵尸进程
僵尸依据电影常识可以解释为死了但是没有死透的人...,一个主进程创建子进程,如果子进程退出了或者终止了,该子进程占用的资源不会被立即释放,因为子进程需要让主进程能够查看在该主进程下开设的子进程的一些基本信息,比如PID等,这样子进程就变成了僵尸进程。
子进程的结束和父进程的运行是一个异步过程,即父进程永远无法预测子进程到底什么时候结束;如果子进程一结束就立刻回收其全部资源,那么在父进程内将无法获取子进程的状态信息。
如果父进程不结束并且无限制的创建子进程,那么在这种情况下僵尸进程就是有害的,占用了系统资源,但是所有的进程都会不如僵尸进程的状态。
孤儿进程
孤儿依据日常生活常识可以解释为没有父母的孩子,在程序中,如果一个父进程结束了,但是它的子进程还在运行,那么还在运行的这些子进程就是孤儿进程。
操作系统会开设一个专门的儿童福利院init进程专门管理孤儿进程进行回收资源。
守护进程
主进程将一个子进程设置为守护进程,守护进程会在主进程结束时一起结束,守护进程内无法开启子进程,必须在开启子进程前将子进程设置为守护进程,如果守护进程执行完毕主进程还在运行,那么对守护进程没有影响,比如古代的皇帝(主进程)和他的妃子(守护进程),皇帝死了(主进程结束),妃子就要陪葬(守护进程随之终止),如果妃子(守护进程)死在皇帝(主进程)前面就算寿终正寝了。代码如下:
from multiprocessing import Process import time def task(name): print(f'{name}娘娘开始侍候皇帝') time.sleep(3) print(f'{name}已经正常死亡') if __name__ == '__main__': p = Process(target=task, kwargs={'name':'王后'}) # 这句话一定要放在start上面,否则会报错 p.daemon = True # 将进程p设置成守护进程 p.start() # time.sleep(5) # 加上这句代码守护进程就会正常结束 print('秦始皇寿终正寝!')
进程同步(锁)
进程之间数据不共享,但是共享同一套文件系统,所以访问同一个文件是没有问题的,但是如果多个进程同时操作一份数据的时候,就会出现数据错乱的问题。
针对上述问题的解决方案就是加锁处理,将并发变为串行,虽然牺牲了效率但是保证了数据的安全,不同的子进程进行抢锁,抢到的就可以继续执行代码,抢不到的需要等上一个人释放锁在进行争抢。
锁的基本使用如下,在主进程中生成一把锁 让所有的子进程抢 谁先抢到谁先使用,抢不到的排队等候下次再抢
from multiprocessing import Lock mutex = Lock() mutex.acquire() # 抢锁 mutex.release() # 释放锁,还锁
我们以抢票为例,说明如何使用锁:
from multiprocessing import Process, Lock import json import random import time class People: def __init__(self,name): self.name = name # 查询余票 def query(self): with open('data','r',encoding='utf-8') as f: dic = json.load(f) num = dic.get('ticket_num') print(f'{self.name} 查询到余票是{num}') return dic # 购买票 def buy(self): dic = self.query() time.sleep(random.randint(1,3)) if dic.get('ticket_num') > 0: dic['ticket_num'] -= 1 with open('data','w',encoding='utf-8') as f: json.dump(dic,f) print(f'{self.name}订票成功') else: print(f'{self.name}订票失败') # 整合上面两个方法 def run(self, mutex): # 先查余票 self.query() # 子进程进行抢锁 mutex.acquire() self.buy() # 购票操作完成后释放锁,由其他子进程继续争抢 mutex.release() if __name__ == '__main__': mutex = Lock() # 获取锁 for i in range(10): p = Process(target=People(i).run,args=(mutex,)) p.start()
假设在字典中存的票数为1张,如果同时有10个子进程查询每个进程都会查到1张票,那么这10个子进程就会同时去买票,如果不加锁的话出现的情况就是每个人都买到了,显然在逻辑上是错误的,但是加锁后就可以避免这个问题的发生。
锁不要轻易使用,只在多个进程争抢数据的环节进行加锁即可。
队列
通过multiprocessing模块导入Queue,使用队列,也可以直接导入 import queue,队列是基于管道+锁实现的,队列不应该存放大数据,而队列中的数据个数受限于内存的大小。
from multiprocessing import Queue # 创建一个队列 q = Queue(5) # 括号内可以传数字 标示生成的队列最大可以同时存放的数据量 q.put(111) q.put(222) q.put(333) # print(q.full()) # 判断当前队列是否满了 # print(q.empty()) # 判断当前队列是否空了 q.put(444) q.put(555) # print(q.full()) # 判断当前队列是否满了 # q.put(666) # 当队列数据放满了之后 如果还有数据要放程序会阻塞 直到有位置让出来 不会报错 """ 存取数据 存是为了更好的取 千方百计的存、简单快捷的取 """ # 去队列中取数据 v1 = q.get() v2 = q.get() v3 = q.get() v4 = q.get() v5 = q.get() # print(q.empty()) # V6 = q.get_nowait() # 没有数据直接报错queue.Empty # v6 = q.get(timeout=3) # 没有数据之后原地等待三秒之后再报错 queue.Empty try: v6 = q.get(timeout=3) print(v6) except Exception as e: print('一滴都没有了!') # # v6 = q.get() # 队列中如果已经没有数据的话 get方法会原地阻塞 # print(v1, v2, v3, v4, v5, v6) """ q.full() q.empty() q.get_nowait() 在多进程的情况下是不精确 """
进程间通信IPC机制
进程通信(IPC,Intent-Process Communication),即进程之间的通信,包括主进程-子进程,子进程-子进程
进程彼此之间互相隔离,要实现进程间通信(IPC),multiprocessing模块支持两种形式:队列和管道,这两种方式都是基于消息传递系统(Message passing system)。推荐使用队列,因为队列是采用管道+锁 实现的。
from multiprocessing import Queue, Process def producer(q): q.put('hello, 很高兴为您服务') def consumer(q): print(q.get()) if __name__ == '__main__': q = Queue() p1 = Process(target=producer, args=(q,)) p2 = Process(target=consumer, args=(q,)) p1.start() p2.start() q.put('start') # 主进程传数据
最后感谢每一个认真阅读我文章的人,礼尚往来总是要有的,这些资料,对于【软件测试】的朋友来说应该是最全面最完整的备战仓库,虽然不是什么很值钱的东西,如果你用得到的话可以直接拿走:
这些资料,对于【软件测试】的朋友来说应该是最全面最完整的备战仓库,这个仓库也陪伴上万个测试工程师们走过最艰难的路程,希望也能帮助到你!