1, 多进程 vs 多线程
Python中的常见的并发模型分为两种:
- 多线程threading并发,多用于IO密集型计算
- 多进程multiprocessing并发,多用于CPU密集型计算
(1)IO密集 vs CPU密集
IO密集:
I/O bound 指的是系统的CPU效能相对硬盘/内存的效能要好很多,此时,系统运作,大部分的状况是 CPU 在等 I/O (硬盘/内存) 的读/写,此时 CPU Loading 不高。
IO密集型,涉及到网络、磁盘IO的任务都是IO密集型任务,这类任务的特点是CPU消耗很少,任务的大部分时间都在等待IO操作完成(因为IO的速度远远低于CPU和内存的速度)。对于IO密集型任务,任务越多,CPU效率越高,但也有一个限度。常见的大部分任务都是IO密集型任务,比如Web应用。
CPU密集:
CPU bound 指的是系统的 硬盘/内存 效能 相对 CPU 的效能 要好很多,此时,系统运作,大部分的状况是 CPU Loading 100%,CPU 要读/写 I/O (硬盘/内存),I/O在很短的时间就可以完成,而 CPU 还有许多运算要处理,CPU Loading 很高。
CPU bound密集型任务的特点是要进行大量的计算,消耗CPU资源,比如计算圆周率、对视频进行高清解码等等,全靠CPU的运算能力。这种计算密集型任务虽然也可以用多任务完成,但是任务越多,花在任务切换的时间就越多,CPU执行任务的效率就越低,所以,要最高效地利用CPU,计算密集型任务同时进行的数量应当等于CPU的核心数。
在多重程序系统中,大部份时间用来做计算、逻辑判断等CPU动作的程序称之CPU bound。例如一个计算圆周率至小数点一千位以下的程序,在执行的过程当中绝大部份时间用在三角函数和开根号的计算,便是属于CPU bound的程序。
(2)多进程 vs 多线程
首先,要实现多任务,通常我们会设计Master-Worker模式,Master负责分配任务,Worker负责执行任务,因此,多任务环境下,通常是一个Master,多个Worker。
如果用多进程实现Master-Worker,主进程就是Master,其他进程就是Worker。
如果用多线程实现Master-Worker,主线程就是Master,其他线程就是Worker。
多进程稳定性好,但是资源代价大
多进程模式最大的优点就是稳定性高,因为一个子进程崩溃了,不会影响主进程和其他子进程。(当然主进程挂了所有进程就全挂了,但是Master进程只负责分配任务,挂掉的概率低)著名的Apache最早就是采用多进程模式。
多进程模式的缺点是创建进程的代价大,在Unix/Linux系统下,用fork
调用还行,在Windows下创建进程开销巨大。另外,操作系统能同时运行的进程数也是有限的,在内存和CPU的限制下,如果有几千个进程同时运行,操作系统连调度都会成问题。
多线程模式通常比多进程快一点,但是也快不到哪去,而且,多线程模式致命的缺点就是任何一个线程挂掉都可能直接造成整个进程崩溃,因为所有线程共享进程的内存。在Windows上,如果一个线程执行的代码出了问题,你经常可以看到这样的提示:“该程序执行了非法操作,即将关闭”,其实往往是某个线程出了问题,但是操作系统会强制结束整个进程。
在Windows下,多线程的效率比多进程要高,所以微软的IIS服务器默认采用多线程模式。由于多线程存在稳定性的问题,IIS的稳定性就不如Apache。为了缓解这个问题,IIS和Apache现在又有多进程+多线程的混合模式,真是把问题越搞越复杂。
多线程CPU切换频率高
操作系统在切换进程或者线程时,它需要先保存当前执行的现场环境(CPU寄存器状态、内存页等),然后,把新任务的执行环境准备好(恢复上次的寄存器状态,切换内存页等),才能开始执行。这个切换过程虽然很快,但是也需要耗费时间。如果有几千个任务同时进行,操作系统可能就主要忙着切换任务,根本没有多少时间去执行任务了,这种情况最常见的就是硬盘狂响,点窗口无反应,系统处于假死状态。所以,多任务一旦多到一个限度,就会消耗掉系统所有的资源,结果效率急剧下降,所有任务都做不好。
Python中多线程的伪多线程
由于GIL,是伪多线程。如果使用多线程,所有的计算只会在一个CPU核上,无法真正利用CPU多核。
想要充分利用多核CPU资源,Python中大部分情况下都需要使用多进程,Python中提供了multiprocessing这个包实现多进程。
2,多进程基本用法
Python中提供了multiprocessing这个包实现多进程。multiprocessing支持子进程、进程间的同步与通信,提供了Process、Queue、Pipe、Lock等组件。
Python的multiprocessing库通过以下几步创建进程:
- 创建进程对象
- 调用
start()
方法,开启进程的活动 - 调用
join()
方法,在进程结束之前一直等待
实例方法:
is_alive():返回进程是否在运行。
join([timeout]):阻塞当前上下文环境的进程程,直到调用此方法的进程终止或到达指定的timeout(可选参数)。
start():启动一个子进程。准备就绪,等待CPU调度
run():不启动子进程,直接执行函数。如果实例进程时未制定传入target,这star执行t默认run()方法。
terminate():不管任务是否完成,立即停止工作进程
属性:
authkey
daemon:和线程的setDeamon功能一样
exitcode(进程在运行时为None、如果为–N,表示被信号N结束)
name:进程名字。
pid:进程号。
和多线程一样,多线程multiprocessing模块也有两种基本办法创建子进程:
- 用法一:Process类来生成进程实例
from multiprocessing import Process
import os
def func(i):
print("pid: {}, execute: {} * {} = {}".format(os.getpid(), i, i, i*i))
if __name__ == "__main__":
data = [1, 2, 3, 4, 5]
process_list = []
for d in data:
p = Process(target=func, args=(d,))
process_list.append(p)
for process in process_list:
process.start()
for process in process_list:
process.join()
- 用法二:继承Process类,自定义进程子类,实现run方法
实现一个自定义的进程子类,需要以下三步:
1>定义 Process
的子类
2>覆盖 __init__(self [,args])
方法来添加额外的参数
3>覆盖 run(self, [.args])
方法来实现 Process
启动的时候执行的任务
from multiprocessing import Process
import os, time
class MyProcess(Process):
def __init__(self, target=None, args=(), kwargs={}):
super(MyProcess, self).__init__()
self.target = target
self.args = tuple(args)
self.kwargs = dict(kwargs)
def run(self):
if self.target:
print("func {} is running at {}".format(self.target.__name__, time.ctime()))
return self.target(*self.args, **self.kwargs)
def func_test(i):
name = multiprocessing.current_process().name
print("{} pid: {}, execute: {} * {} = {}".format(name, os.getpid(), i, i, i * i))
if __name__ == "__main__":
data = [1, 2, 3, 4, 5]
process_list = []
for d in data:
p = MyProcess(target=func, args=(d,))
process_list.append(p)
for process in process_list:
process.start()
for process in process_list:
process.join()
3,多进程数据同步原语
进程的同步原语和线程的库很类似:
- Lock: 这个对象可以有两种装填:锁住的(locked)和没锁住的(unlocked)。一个Lock对象有两个方法,
acquire()
和release()
,来控制共享数据的读写权限。 - Event: 实现了进程间的简单通讯,一个进程发事件的信号,另一个进程等待事件的信号。
Event
对象有两个方法,set()
和clear()
,来管理自己内部的变量。 - Condition: 此对象用来同步部分工作流程,在并行的进程中,有两个基本的方法:
wait()
用来等待进程,notify_all()
用来通知所有等待此条件的进程。 - Semaphore: 用来共享资源,例如,支持固定数量的共享连接。
- Rlock: 递归锁对象。其用途和方法同
Threading
模块一样。 - Barrier: 将程序分成几个阶段,适用于有些进程必须在某些特定进程之后执行。处于障碍(Barrier)之后的代码不能同处于障碍之前的代码并行。(Python 3.3以后版本支持)
Lock,Rlock,Event,Condition,Semaphore几个进程同步原语的用法和多线程基本完全一致,只需要将threading.Thread对象换成multiprocessing.Process对象即可。
请参考前一篇博客:https://blog.csdn.net/biheyu828/article/details/83019392
示例代码:
使用Barrier栅栏控制多进程并发执行
import multiprocessing
import time
from multiprocessing import Process
from multiprocessing import Barrier, Lock
def run_with_barrier(barrier):
proc_name = multiprocessing.current_process().name
barrier.wait() ##当两个进程p都调用 wait() 方法的时候,它们会一起继续执行
time.sleep(3)
print("process {} ----> {}".format(proc_name, time.time()))
def run_without_barrier():
proc_name = multiprocessing.current_process().name
time.sleep(3)
print("process {} ----> {}".format(proc_name, time.time()))
if __name__ == "__main__":
barrier = Barrier(2)
lock = Lock()
process_list = []
pro_1 = Process(name="process_1_barrier",target=run_with_barrier, args=(barrier,))
pro_2 = Process(name="process_2_barrier",target=run_with_barrier, args=(barrier,))
pro_3 = Process(name="process_3_no_barrier",target=run_without_barrier)
pro_4 = Process(name="process_4_no_barrier",target=run_without_barrier)
process_list.append(pro_1)
process_list.append(pro_2)
process_list.append(pro_3)
process_list.append(pro_4)
for pro in process_list:
pro.start()
for pro in process_list:
pro.join()
运行结果:
process process_3_no_barrier ----> 1540048781.565112
process process_1_barrier ----> 1540048781.565113
process process_2_barrier ----> 1540048781.565102
process process_4_no_barrier ----> 1540048781.5670989
从运行结果可以看出pro_1和pro_2到达了barrier几乎同时运行,但是pro_3和pro_4进程时间差别较大。
4,多进程交换数据(queue/ pipe)
不同进程之间内存是不共享的。在多进程中直接使用线程类似的方式共享数据,会出现报错,全局变量并不能在不同进程间共享。