day25 多进程
今日内容
- 多任务
- 多进程
- 进程池
昨日回顾
- 魔法方法
__init__
__new__
__str__
__len__
__del__
__eq__
__hash__
- 异常处理
try...except...
try...except...except...
try...except...else...
finally
今日内容详细
多任务
我们打开任务管理器,就会发现,同一时刻用很多程序在运行。这种是的计算机可以同时处理多个任务的现象就是多任务处理。
有了多任务处理,我们才能做到在听歌的同时使用QQ聊天、办公和下载文件。
多任务处理的几个重要概念
- 串行:程序依照先后顺序,逐个执行,一次只能执行一个任务
- 并行:多个程序同时执行,需要CPU数目多于任务数才能实现
- 并发:在一段时间内,多个任务一起执行,因为时间很短,看起来就像同时执行一样,程序数可以多与CPU数
- 同步:一个程序执行完再调用另一个程序,要等程序执行完毕才能开始下一个程序
- 异步:一个程序执行中就调用另一个程序,不需要程序执行完就可以开启下一个程序,只有异步的情况才能实现并发
- 阻塞:CPU不工作
- 非阻塞:CPU工作
多进程
程序,是一个指令的集合,也就是我们写的一套代码。
进程则是指正在执行的程序。换句话说,当你运行一个程序,你就启动了一个进程。
- 编写玩的代码,没有运行时,称为程序,正在运行的代码,称为进程
- 程序是死的(静态的),继承是活的(动态的)
操作系统轮流让各个任务交替执行。由于CPU的执行速度实在是太快了,我们感觉就像所有任务都在同时执行一样。
多进程,就是让多个程序同时运行。多进程中,每个进程中所有数据(包括全局变量)都各拥有一份,互不影响。
程序开始运行时,会首先创建一个主进程(父进程)
在主进程下,我们可以创建新的进程,也就是子进程。
子进程依赖于主进程,如果主进程结束,程序会退出,子进程也就自动终止。
Python提供了非常好用的多进程包multiprocessing。借助这个包,可以轻松完成单进程到并发执行的转换:
from multiprocessing import Process
def land_occupation(name):
print(f'{name}占领铜锣湾')
def grab_the_ground():
print('抢占钵兰街')
# Windows系统需要这行代码避免迭代导入的异常
if __name__ == '__main__':
print('主进程启动,帮派建立')
haonan = Process(target=land_occupation, args=('陈浩南',), name='陈浩南占地盘的子进程')
# target表示调用的方法,args表示调用方法的位置参数元组
# 需要注意的是,如果元组只有一个元素,括号中需要加一个逗号
shisanmei = Process(target=grab_the_ground, name='十三妹抢地盘的子进程')
print(haonan.name, shisanmei.name)
print(haonan.pid, shisanmei.pid)
haonan.start()
shisanmei.start()
print(haonan.pid, shisanmei.pid)
haonan.join()
shisanmei.join()
print('程序结束')
输出的结果为:
主进程启动,帮派建立
陈浩南占地盘的子进程 十三妹抢地盘的子进程
None None
19932 19160
抢占钵兰街
陈浩南占领铜锣湾
程序结束
在上面的代码中,我们把创建的进程和调用进程的代码都写到了if __name__ == '__main__':
的子句中。
这是因为在Windows中,子进程会自动import启动它的文件。而在import时,文件中的代码会被自动执行。当执行到创建子进程的代码时,又要重新导入自己。这就陷入了一个导入自己的死循环,然后就会报错。
为了避免报错,我们把这些代码放到if __name__ == '__main__':
的子句中,这样当创建子进程时,导入代码就不会重新加载创建子进程的语句了。
不过最好还是将操作多进程的代码封装到函数中,这样会避免很多麻烦。
Process(target, name, args)
参数介绍
- target表示调用的对象,即子进程要执行的任务
- args表示要调用对象的位置参数组成的元组(经测试,只要是可迭代对象都可以。需要注意的是,传入的内容会被迭代运行,所以要注意避免误传参数的问题)
- name为自己成的名字,默认为Process-1等等
Process类常用方法
.start()
:启动进程,并调用子进程中的.run()
方法.run()
:进程启动时调用的方法,正是它去调用target参数指定的函数。.terminate()
:(了解即可)强制终止进程,不会进行任何清理操作,进程会一直占用内存空间.is_alive()
:如果进程仍在运行,返回True,否则返回False。用来判断进程是否还在运行.join([timeout])
:中近程等待子进程终止,timeout时可选的超时时间
Process类常用属性:
- name:当前进程实例的别名,默认为Process-N,N为从1开始递增的整数
- pid:当前进程实例的PID值,也就是操作系统为该进程进行的编号。pid在程序就绪之前(start方法未执行)的状态时值为None。只有当进程就绪,才会被分配PID值。
全局变量在多个进程中是不共享的。进程之间的数据相互独立,默认情况下不会相互影响:
from multiprocessing import Process
num = 10
def r1():
global num
num += 5
print('在进程一中,num的值为:', num)
def r2():
global num
num += 10
print('在进程二中,num的值为:', num)
if __name__ == '__main__':
p1 = Process(target=r1)
p2 = Process(target=r2)
p1.start()
p2.start()
p1.join()
p2.join()
print('程序结束前,主进程中num的值为:', num)
输出的结果为:
在进程一中,num的值为: 15
在进程二中,num的值为: 20
程序结束前,主进程中num的值为: 10
需要注意的是,每次创建进程对象时,都会import当前文件。这就导致在if __name__ == '__main__'
语句之后对num进行的修改操作不会被加载到进程对象的内存中。换句话说,子进程能够加载全局变量在if __name__ == '__main__'
语句之外的修改,却无法加载其内部的修改:
from multiprocessing import Process
num = 10
def r1():
global num
num += 5
print('在进程一中,num的值为:', num)
def r2():
global num
num += 10
print('在进程二中,num的值为:', num)
# 在外部修改全局变量num
num += 500
if __name__ == '__main__':
# 在内部修改全局变量
num += 1000
p1 = Process(target=r1)
p2 = Process(target=r2)
p1.start()
p2.start()
p1.join()
p2.join()
print('程序结束前,主进程中num的值为:', num)
输出的结果为:
在进程一中,num的值为: 515
在进程二中,num的值为: 520
程序结束前,主进程中num的值为: 1510
在子进程中,1000都没有被加上,但是500被加上了。由此可以验证前面的观点。
除了直接使用Process创建类对象之外,我们还可以自己写进程对象,只需继承Process类即可:
from multiprocessing import Process
import time
class ClockProcess(Process):
# 重写的run方法就是我们要执行的子进程方法
def run(self):
n = 5
while n > 0:
print(n)
time.sleep(1)
n -= 1
if __name__ = '__main__':
p = ClockProcess()
p.start()
p.join()
输出的结果为:
5
4
3
2
1
上面的内容每隔一秒钟打印出一个
进程池
进程池用来创建多个进程。
当需要创建的子进程数量不多时,可以直接利用multiprocessing中的Process动态生成多个进程。但如果我们需要创建大量的进程,如果手动创建那工作量将极其巨大。而且会产生很多的重复代码。此时,就可以用到multiprocessing模块提供的Pool来实现批量创建多个进程。
初始化Pool时,可以指定一个最大进程数。当有新的请求提交到Pool中时,如果池还没有满,那么就会创建一个新的进程用来执行该请求;如果池中的进程数已经达到指定的最大值,那么该请求就会等待,直到池中有进程结束,才会创建新的进程来执行。
创建进程池的基本办法为:
from multiprocessing import Pool
import time
def r1():
print('这里是进程一呀~')
time.sleep(5)
def r2():
print('这里是进程二呀~')
time.sleep(3)
if __name__ == '__main__':
# 定义一个进程池,参数为最大进程数,默认大小为CPU核数
po = Pool()
for i in range(100):
# apply_async选择要调用的目标,每次循环会用空出来的子进程去调用目标
po.apply_async(r1)
po.apply_async(r2)
# 进程池关闭后不再接收新的请求
po.close()
# 等待po中所有子进程结束,必须放在close后面
po.join()
# 在多进程中,主进程一般用来等待,真正的任务都在子进程中
multiprocessing.Pool
常用函数解析
apply_async(func[, args[, kwargs]])
:使用非阻塞方式调用func(并行执行,阻塞方式必须等待上一个进程退出才能执行下一个进程)。args为传递给func的位置参数,kwargs为传递给func 的关键字参数apply(func[, args[, kwargs]])
(了解即可)使用阻塞方式调用func,效果与单进程相同close()
:关闭进程池,使其不再接收新的任务join()
:主进程阻塞,等待子进程的退出,必须在close或terminate之后使用