多进程编程
1.什么是进程
进程是系统中正在运行的一个程序,程序一旦运行就是进程。
进程可以看成程序执行的一个实例。进程是系统资源分配的独立实体,每个进程都拥有独立的地址空间。一个进程无法访问另一个进程的变量和数据结构,如果想让一个进程访问另一个进程的资源,需要使用进程间通信,比如管道,文件,套接字等。
一个进程可以拥有多个线程,每个线程使用其所属进程的栈空间。线程与进程的一个主要区别是,统一进程内的一个主要区别是,同一进程内的多个线程会共享部分状态,多个线程可以读写同一块内存(一个进程无法直接访问另一进程的内存)。同时,每个线程还拥有自己的寄存器和栈,其他线程可以读写这些栈内存。
2.进程的层次结构
无论UNIX还是windows,进程只有一个父进程,不同的是:
1. 在UNIX中所有的进程,都是以init进程为根,组成树形结构。父子进程共同组成一个进程组,这样,当从键盘发出一个信号时,该信号被送给当前与键盘相关的进程组中的所有成员。
2. 在windows中,没有进程层次的概念,所有的进程都是地位相同的,唯一类似于进程层次的暗示,是在创建进程时,父进程得到一个特别的令牌(称为句柄),该句柄可以用来控制子进程,但是父进程有权把该句柄传给其他子进程,这样就没有层次了。
3.进程并发的现象
进程并发的实现在于,硬件中断一个正在运行的进程,把此时进程运行的所有状态保存下来,为此,操作系统维护一张表格,即进程表(process table),每个进程占用一个进程表项(这些表项也称为进程控制块)
4.multiprocessing模块介绍
multiprocessing模块包含一个API,它基于threading API可以在多个进程中划分工作。由于Python全局解释器锁,python无法利用多个内核,我们可以通过multiprocessing来代替threading来利用多个CPU内核,进而解决计算瓶颈。
multiprocessing的功能众多:支持子进程、通信和共享数据、执行不同形式的同步,提供了Process、Queue、Pipe、Lock等组件。
Process类的参数介绍 | 描述 |
---|---|
group | 参数未使用,值始终为None |
target | 表示调用对象,即子进程要执行的任务 |
args | 表示调用对象的位置参数元组。如果没有参数,则为一个空元组 |
name | 子进程的名称 |
Process类的方法介绍 | |
---|---|
start() | 启动进程,并调用该子进程中的p.run() |
run() | 进程启动时运行的方法,正是它去调用target指定的函数,我们自定义类的类中一定要实现该方法 |
terminate() | 强制终止进程p,不会进行任何清理操作,如果p创建了子进程,该子进程就成了僵尸进程,使用该方法需要特别小心这种情况。 |
is_alive() | 如果子进程仍然运行,返回True |
join([timeout]) | 主线程等待子进程终止(强调:是主线程处于等的状态,而子进程是处于运行的状态)。timeout是可选的超时时间, |
需要强调的是,p.join只能join住start开启的进程,而不能join住run开启的进程 |
Process类的属性介绍 | 描述 |
---|---|
daemon | 默认值为False,如果设为True,代表p为后台运行的守护进程,当p的父进程终止时,p也随之终止,并且设定为True后,p不能创建自己的新进程,必须在p.start()之前设置 |
name | 进程的名称 |
pid | 进程的pid |
ppid | 父进程的ip |
5.multiprocessing模块的使用
开启进程的两种方式
第一种,创建子进程,最简单的方法就是用一个目标函数实例化一个Process对象,并调用start()方法让它开始工作。
#示例1
import multiprocessing
import time
def worker(name):
print("%s is working,id:%d" % (self.name, self.pid))
time.sleep(2)
print("%s is work ending,id:%d" % (self.name, self.pid))
if __name__ == '__main__':
jobs = []
p1 = multiprocessing.Process(target=worker, args= ("xiaoming",))
p2 = multiprocessing.Process(target=worker, args= ("dong",))
p1.start()
p2.start()
print('主进程')
第二种方法,我们可以通过Process派生出一个定制子类。
mport multiprocessing
import time
class MyProcess(multiprocessing.Process):
def __init__(self, name):
super().__init__() # 必须继承父类的一些属性
self.name = name
def run(self):
print("%s is working,id:%d" % (self.name,self.pid))
time.sleep(2)
print("%s is work ending,id:%d" % (self.name,self.pid))
if __name__ == '__main__':
jobs = []
p1 = MyProcess("xiaoming")
p2 = MyProcess("dong")
p1.start()
p2.start()
print('主进程')
互斥锁
进程之间数据隔离,但是共享一套文件系统,因而可以通过文件来实现进程直接的通信,但问题是必须自己加锁处理。
注意:加锁的目的是为了保证多个进程修改同一块数据时,同一时间只能有一个修改,即串行的修改,没错,速度是慢了,牺牲了速度而保证了数据安全。
import multiprocessing
import os
import time
def work(mutex):
mutex.acquire()
print('task[%s] is working'%os.getpid())
time.sleep(3)
print('task[%s] is work ending'%os.getpid())
mutex.release()
if __name__ == '__main__':
mutex = multiprocessing.Lock()
p1 = multiprocessing.Process(target=work,args=(mutex,))
p2 = multiprocessing.Process(target=work,args=(mutex,))
p1.start()
p2.start()
p1.join()
p2.join()
print('主')
确定当前进程
传递参数来标识或命名进程很麻烦,而且没有必要。每个Process示实例都有一个名称,其默认值可以在创建进程时改变。给进程命名对于追踪进程是很有帮助的,特别是当应用中有多种类型的进程同时运行时。
import multiprocessing
import time
def work():
name = multiprocessing.current_process().name
print(name, "Starting")
time.sleep(2)
print(name, "Exiting")
def my_service():
name = multiprocessing.current_process().name
print(name, "Starting")
time.sleep(3)
print(name, "Exiting")
if __name__ == '__main__':
service = multiprocessing.Process(name="my_service",
target=my_service,
args=())
work_1 = multiprocessing.Process(name = "work_1",
target=work,
args=())
work_2 = multiprocessing.Process(target=work,
args=())
work_1.start()
work_2.start()
service.start()
调试输出中,每行都包含当前进程的名称。进程名称列为Process-3的行对应为命名的进程work_1。对于未命名的进程,系统默认命名为 Process- 的形式。
守护进程
默认情况下,在所有子进程退出之前主程序不会退出。有些情况下,可能需要启动一个后台进程。它可以一直运行而步阻塞主进程退出,如果一个服务无法用一个容易的方法来中断进程,或者希望进程工作到一半时中止而不损失或破坏数据,对于这些服务,使用守护进程就很有用了。
要标识一个进程为守护进程,可以将其daemon属性设置为True。默认情况下进程不作为守护进程。
import multiprocessing
import time
import sys
def daemon():
p = multiprocessing.current_process()
print("Starting:", p.name, p.pid)
sys.stdout.flush()
time.sleep(2)
print("Exiting:", p.name, p.pid)
sys.stdout.flush()
def non_daemon():
p = multiprocessing.current_process()
print("Starting:", p.name, p.pid)
sys.stdout.flush()
print("Exiting:", p.name, p.pid)
sys.stdout.flush()
if __name__ == '__main__':
d = multiprocessing.Process(name="daemon",
target=daemon)
d.daemon = True
n = multiprocessing.Process(name = "non_daemon",
target=non_daemon)
n.daemon = False
d.start()
time.sleep(2)
n.start()
调试输出中,没有守护进程的Exiting消息,因为在守护进程从其2秒的睡眠时间唤醒之前,所有的非守护进程(包括主进程)已经退出。
终止进程
如果一个进程看起来已经挂起或者陷入死锁,则需要能够强制性地将其结束。对一个进程对象调用terminate()会结束子进程。
import multiprocessing
import time
def slow_worker():
print("Starting worker")
time.sleep(0.1)
print("Finished worker")
if __name__ == '__main__':
p = multiprocessing.Process(target=slow_worker)
print("Before:", p , p.is_alive())
p.start()
print("During", p ,p.is_alive())
p.terminate()
print("Terminated", p, p.is_alive())
p.join()
print("Joined:", p, p.is_alive())
注意点:终止进程后要用join()退出进程,使进程管理代码有时间更新对象的状态,以反映进程已经终止。
6.进程通信
进程通信有3种模式:
- 通过multiprocessing.Queue(),该模式的数据通信会消耗大量资源。
- 管道multiprocessing.Pipe()
- 数据共享 Managers()
Queue队列传递
类似于线程,对于多个进程,一种常用的模式是将一个工作划分为多个工作进程中并行地运行。要想有效地使用多个进程,通常要求它们之间有某种通信,这样它们才能够分解工作,并完成结果的汇总。利用multiprocessing完成进程间的通信的一种简单的方法就是使用一个Queue来回传递消息。
import time
from multiprocessing import Process
import multiprocessing
def foo(q):
time.sleep(2)
q.put(12)
q.put("baidu")
q.put({"name": "baidu"})
if __name__ == '__main__':
q = multiprocessing.Queue()
p = Process(target=foo, args=(q,))
p.start()
print(q.get())
print(q.get())
print(q.get())
Pipe管道通信
管道相当于队列,但是管道不自动加锁
import multiprocessing
def f(conn):
conn.send("Hello Dad")
response = conn.recv()
print(response)
print("来自子进程的conn_id",id(conn))
conn.close()
if __name__ == '__main__':
parent_conn, son_conn = multiprocessing.Pipe() #双向管道,返回到是一个元组的形式
p = multiprocessing.Process(target=f, args=(son_conn,))
p.start()
print("parent_conn_id",id(parent_conn))
print("来自父进程的son_conn_id",id(son_conn))
print(parent_conn.recv())
parent_conn.send("hello")
p.join()
需要注意的是,这里的发送消息与socket模块中的send是不一样的,虽然两个模块的接口一样。因为socket模块中的send发送消息需要通过底层,那么只能通过字节的形式来发送。与进程中的数据共享有点区别。
Managers数据共享
Queue和Pipe只实现了的是数据交互,并没有实现数据共享,即一个进程更改另一个进程的数据。
通过Manager创建的特殊类型列表对象集中维护活动进程列表。Manager负责协调其所有用户之间的共享信息状态。
通过Managers()管理器来创建列表,这个列表将会共享,所有进程都能看到列表更新。除了列表,管理器还支持字典。
import multiprocessing
def worker(d, key, value):
d[key] = value
if __name__ == '__main__':
manager = multiprocessing.Manager()
# d = {}
d = manager.dict()
jobs = [ multiprocessing.Process(target=worker, args=(d, i, i*2 )) for i in range(10)]
for j in jobs:
j.start()
for j in jobs:
j.join()
print("Results:",d)
共享命名空间,除了字典和列表之外,Manager还可以创建一个共享Namespace。
import multiprocessing
def producer(ns, event):
ns.value = "This is the value"
event.set()
def consumer(ns, event):
try:
value = ns.value
except Exception as err:
print("Before event, error:",err)
event.wait()
print("After event:", ns.value)
if __name__ == '__main__':
manager = multiprocessing.Manager()
namespace = manager.Namespace()
event = multiprocessing.Event()
p = multiprocessing.Process(target=producer, args=(namespace, event))
c = multiprocessing.Process(target=consumer, args=(namespace, event))
c.start()
p.start()
c.join()
p.join()
添加到Namespace的所有命名值对所有接收Namespace实例的客户可见。但是对命名空间种可变值内容的更新不会自动传播。
import multiprocessing
def producer(ns, event):
ns.my_list.append("This is the value")
event.set()
def consumer(ns, event):
print("Before event:", ns.my_list)
event.wait()
print("After event:", ns.my_list)
if __name__ == '__main__':
manager = multiprocessing.Manager()
namespace = manager.Namespace()
namespace.my_list = []
event = multiprocessing.Event()
p = multiprocessing.Process(target=producer,
args=(namespace, event))
c = multiprocessing.Process(target=consumer,
args=(namespace, event))
c.start()
p.start()
c.join()
p.join()
如果要更新这个列表,那么就需要将它再次关联到命名空间对象。
7.进程池
有些情况下,所要完成的工作可以分解并独立地分布到多个工作进程,对于这种简单地情况,可以使用Pool类来管理固定数目的工作进程。多进程是实现并发的手段之一,需要注意的问题是:
- 很明显需要并发执行的任务通常要远大于核数
- 一个操作系统不可能无限开启进程,通常有几个核就开几个进程,进程开启过多,效率反而会下降(开启进程是需要占用系统资源的,而且开启多余核数目的进程也无法做到并行)
那么什么是进程池呢?进程池就是控制进程数目
ps:对于远程过程调用的高级应用程序而言,应该使用进程池,Pool可以提供指定数量的进程,供用户调用,当有新的请求提交到pool中时,如果池还没有满,那么就会创建一个新的进程用来执行该请求;但如果池中的进程数已经达到规定最大值,那么该请求就会等待,直到池中有进程结束,就重用进程池中的进程。
Pool类的参数 | 描述 |
---|---|
process | 要创建的进程数,如果省略,将默认为cpu_count()的值,可os.cpu_count()查看 |
initializer | 是每个工作进程启动时要执行的可调用对象,默认为None |
initargs | 是要传给initializer的参数组 |
maxtasksperchild | 表示每个进程执行task的最大数目,设置maxtasksperchild参数可以告诉池在完成一定数量任务之后重新启动一个工作进程,来避免运行时间很长的工作进程消耗太多的系统资源。 |
ontext |
Pool类的方法 | 描述 |
---|---|
apply_async (func, args=(), kwds={}, callback=None,error_callback=None) | 使用非阻塞方式调用指定方法,并行执行(同时执行)。如果callback指定,当结果可用时,结果会调用callback,callback属于主进程 。注意点:返回的值为一个对象,返回值用.get方法获取。 |
apply (func, args=(), kwds={}) | 使用阻塞方式调用指定方法,阻塞方式就是要等上一个进程退出后,下一个进程才开始运行。 |
close() | 关闭进程池,不再接受进的进程请求,但已经接受的进程还是会继续执行。 |
terminate() | 不管程任务是否完成,立即结束。 |
join() | 主进程堵塞(就是不执行join下面的语句),直到子进程结束,注意,该方法必须在close或terminate之后使用。 |
map(func,iterable,chunksize) | 将可调用对象func应用给iterable的每一项,然后以列表形式返回结果,通过将iterable划分为多块,并分配给工作进程,可以并行执行。chunksize指定每块中的项数,如果数据量较大,可以增大chunksize的值来提升性能。 |
map_async(func,iterable,chunksize,callback) | 与map方法不同之处是返回结果是异步的,如果callback指定,当结果可用时,结果会调用callback,callback属于主进程 。 |
imap(func,iterable,chunksize) | 与map()方法的不同之处是返回迭代器而非列表。 |
imap_unordered(func,iterable,chunksize) | 与imap()不同之处是:结果的顺序是根据从工作进程接收到的时间而定的。 |
get(timeout) | 如果没有设置timeout,将会一直等待结果,如果设置了timeout,超过timeout将引发multiprocessing.TimeoutError异常。 |
ready() | 如果调用完成,返回True |
successful() | 如果调用完成并且没有引发异常,返回True,如果在结果就绪之前调用,将引发AssertionError异常。 |
wait(timeout) | 等待结果变为可用,timeout为等待时间。 |
apply()
mport multiprocessing
import os,time
def task(n):
print('task[%s] is working'%os.getpid())
time.sleep(2)
print('task[%s] finished'%os.getpid())
return n**2
if __name__ == '__main__':
p = multiprocessing.Pool(4) #最大四个进程
res_list = []
for i in range(10):#
res = p.apply(task,args=(i,)) #同步的,等着一个运行完才执行另一个
res_list.append(res)
print('本次任务的结束:',res_list)
p.close()#禁止往进程池内在添加任务
p.join() #在等进程池
print('主')
apply()方法是阻塞主进程, 并且一个一个按顺序地执行子进程, 等到全部子进程都执行完毕后 ,继续执行 apply()后面主进程的代码。
apply_async()
#错误版本
import multiprocessing
import os,time
def task(n):
print('task[%s] running...'%os.getpid())
time.sleep(3)
return n**2
if __name__ == '__main__':
start = time.time()
p = multiprocessing.Pool(4)
res_obj_l = []
for i in range(10):
res = p.apply_async(task,args=(i,)) #res为一个对象,若要取得task的返回值,则用.get方法
res_obj_l.append(res.get())
p.close() #禁止往进程池里添加任务
# p.join()
print(res_obj_l)
print(time.time()-start)
其实我们通过现象发现该程序是阻塞的,即便没有p.join()方法。该程序也会等子进程运行结束后才能够运行主程序,那么,阻塞的意义何在?
原因是apply_async后面 get()等待线程运行结束才会下一个,而apply_async刚好又是异步以主程序为主的,所以这段代码实际变成了阻塞。
#正确版本
import multiprocessing
import os,time
def task(n):
print('task[%s] running...'%os.getpid())
time.sleep(3)
return n**2
if __name__ == '__main__':
start = time.time()
p = multiprocessing.Pool(4)
res_obj_l = []
for i in range(10):
res = p.apply_async(task,args=(i,)) #res为一个对象,若要取得task的返回值,则用.get方法
res_obj_l.append(res)
p.close() #禁止往进程池里添加任务
p.join()
print([obj.get() for obj in res_obj_l]) #这样就得到了就得到了
print(time.time()-start)