Python 多进程
Python 多进程
为什么要采用多进程呢?
如果是计算密集型,在多核cpu情况下,如果是只有一个进程在跑,那么只能利用到一个cpu进行工作,但是如果用多进程的方式,则就能充分利用多核cpu的优势,但并不是进程数量越多越好,因为CPU切换进程是有损耗的,那么如果频繁去切换进程,则效率反而没有两个进程来得实在(一切前提都建立在处理计算密集型工作)
Python 多进程库multiprocessing介绍
multiprocessing 是一个支持使用 API 来产生进程的包,同时提供了本地和远程并发操作,通过使用子进程而非线程有效地绕过了GIL锁(全局解释器锁)。
multiprocessing 能让程序充分利用多核CPU的计算优势。
multiprocessing 多进程启动
通过实例化Process来创建子进程,并且将一些相关的参数初始化,target参数指定的是子进程执行的目标函数, 调用实例函数start()启动子进程。
import os
from multiprocessing import Process
def process_test(parm):
print(f"========{parm} pid:{os.getpid()}==========")
if __name__ == '__main__':
# 初始化子进程
p = Process(target=process_test, args=("child", ))
# 运行子进程
p.start()
parm = "father"
print(f"========{parm} pid:{os.getpid()}==========")
守护进程(daemon)
当主进程结束时会终止所有守护进程,我个人理解是,当子进程设置daemon=True时,就表明这个进程由主进程来守护,当主进程被销毁时,他守护的进程都会终止 。
daemon设置要放在start之前,不然会抛出AssertionError,可以理解为在子进程运行之前设置的一些参数,但如果启动了,那么久无法设置守护进程这个参数了。
import os
import time
from multiprocessing import Process
def process_test(parm):
time.sleep(1)
print(f"========{parm} pid:{os.getpid()}==========")
if __name__ == '__main__':
# 初始化子进程
p1 = Process(target=process_test, args=("child",))
# 守护进程
p1.daemon = True
# 运行子进程
p1.start()
parm = "father"
print(f"========{parm} pid:{os.getpid()}==========")
join()
调用该方法的子进程会造成主进程阻塞,一直到子进程终止,可以设置超时时间来规定阻塞的时间。
import os
import time
from multiprocessing import Process
def process_test(parm):
time.sleep(2)
print(f"========{parm} pid:{os.getpid()}==========")
if __name__ == '__main__':
# 初始化子进程
p1 = Process(target=process_test, args=("child",))
# 守护进程
p1.daemon = True
# 运行子进程
p1.start()
# 阻塞主进程,如果超时时间设置比子进程实际所需实际长,那么就只会等待1S
p1.join(1)
# 如果不设置超时时间,那么主进程会一直阻塞至子进程终止
# p1.join()
parm = "father"
print(f"========{parm} pid:{os.getpid()}==========")
is_alive()
判断子进程是否存活,生命周期是从调用start()函数开始直至子进程终止。
import os
import time
from multiprocessing import Process
def process_test(parm):
time.sleep(2)
print(f"========{parm} pid:{os.getpid()}==========")
if __name__ == '__main__':
# 初始化子进程
p1 = Process(target=process_test, args=("child",))
# 守护进程
p1.daemon = True
# 此时子进程还未启动所以打印False
print(p1.is_alive())
# 运行子进程
p1.start()
print(p1.is_alive())
# 阻塞主进程
p1.join()
parm = "father"
print(f"========{parm} pid:{os.getpid()}==========")
terminate() or kill()
终止子进程,调用后不会立即生效,操作系统需要时间去关闭这个子进程,如果调用完terminate() or kill() 后立马执行is_alive()还是一样返回True,原因是该进程还未完全关闭。
这两个方法的区别在于unix上调用的信号量不一样
terminate --> SIGTERM
kill -->SIGKILL
terminate demo:
import os
import time
from multiprocessing import Process
def process_test(parm):
time.sleep(2)
print(f"========{parm} pid:{os.getpid()}==========")
if __name__ == '__main__':
# 初始化子进程
p1 = Process(target=process_test, args=("child",))
# 守护进程
p1.daemon = True
p1.start()
print(p1.is_alive())
p1.terminate()
# 增加0.1s延时,让操作系统有时间关闭这个子进程
time.sleep(0.1)
print(p1.is_alive())
parm = "father"
print(f"========{parm} pid:{os.getpid()}==========")
kill demo:
import os
import time
from multiprocessing import Process
def process_test(parm):
time.sleep(2)
print(f"========{parm} pid:{os.getpid()}==========")
if __name__ == '__main__':
# 初始化子进程
p1 = Process(target=process_test, args=("child",))
# 守护进程
p1.daemon = True
p1.start()
print(p1.is_alive())
p1.kill()
time.sleep(0.1)
print(p1.is_alive())
parm = "father"
print(f"========{parm} pid:{os.getpid()}==========")
close()
销毁子进程的实例对象,只有子进程完全终止后才能调用,否则会抛出ValueError。
import os
import time
from multiprocessing import Process
def process_test(parm):
time.sleep(2)
print(f"========{parm} pid:{os.getpid()}==========")
if __name__ == '__main__':
# 初始化子进程
p1 = Process(target=process_test, args=("child",))
# 守护进程
p1.daemon = True
p1.start()
print(p1.is_alive())
# p1.terminate()
p1.join()
time.sleep(0.1)
p1.close()
parm = "father"
print(f"========{parm} pid:{os.getpid()}==========")
IPC(进程间通信)
由于进程之间的内存是相互独立的,但在业务场景中又存在网络受限或者其他各种原因导致无法使用数据库或者其他类似于kafka等相对较大的组件时,那么就可以使用进程间通信来解决问题。
python 多进程间的通信有四种方式Pipe、Queue、共享内存
Pipe
返回一对 Connection 对象 (conn1, conn2) , 分别表示管道的两端。默认使用的是全双工管道,也就是这两个连接既可以发送也可以接收信息,如果将duplex射成False,conn1只能用于接收,conn2只能用于发送。
Connection 底层是通过socket实现,提供api的方式调用。
from multiprocessing import Pipe
from multiprocessing import Process
import time
def consumer(conn_2):
time.sleep(1)
print(conn_2.recv())
conn_2.send("<-----------I have received it")
def producer(conn_1):
conn_1.send("send message------->")
print(conn_1.recv())
if __name__ == '__main__':
conn_1, conn_2 = Pipe()
pro = Process(target=producer, args=(conn_1,))
con = Process(target=consumer, args=(conn_2,))
pro.daemon = True
con.daemon = True
pro.start()
con.start()
pro.join()
con.join()
如果对端关闭了close()连接或者没有东西可接收,将抛出 EOFError 异常。
from multiprocessing import Pipe
from multiprocessing import Process
import time
def consumer(conn_2):
time.sleep(1)
print(conn_2.recv())
print(conn_2.recv())
conn_2.send("<-----------I have received it")
def producer(conn_1):
conn_1.send("send message------->")
conn_1.close()
if __name__ == '__main__':
conn_1, conn_2 = Pipe()
pro = Process(target=producer, args=(conn_1,))
con = Process(target=consumer, args=(conn_2,))
pro.daemon = True
con.daemon = True
pro.start()
con.start()
# 若提前关闭连接1,则连接2仍在接收请求则会抛出EOFError
conn_1.close()
pro.join()
con.join()
poll()返回连接对象中是否有可以读取的数据。
from multiprocessing import Pipe
from multiprocessing import Process
import time
def consumer(conn_2):
while not conn_2.poll():
print("wait message---------->")
time.sleep(1)
print(conn_2.recv())
conn_2.send("<-----------I have received it")
def producer(conn_1):
time.sleep(10)
conn_1.send("send message------->")
conn_1.close()
if __name__ == '__main__':
conn_1, conn_2 = Pipe()
pro = Process(target=producer, args=(conn_1,))
con = Process(target=consumer, args=(conn_2,))
pro.daemon = True
con.daemon = True
pro.start()
con.start()
pro.join()
con.join()
Queue
Queue, SimpleQueue 以及 JoinableQueue 都是多生产者,多消费者,并且实现了 FIFO 的队列类型.
底层是通过封装一个半双工的pipe管道及一些相关的锁和信号量实现的。当一个进程将一个对象放进队列中时,一个写入线程会启动并将对象从缓冲区写入管道中。该对象可以设置队列的最大长度,如果没有传入maxsize则默认是2147483647
put() and get()
生产者调用put()方法将数据写入队列中,消费者通过get()方法队列中的数据
put方法如果队列满了则默认会阻塞住,等待队列被消费后继续写入,如果不想阻塞可以加入参数block=False,那么此时会抛出queue.Full的错误
get方法在队列为空时默认会阻塞住,等待队列有数据时获取数据,如果不想阻塞可以加入参数block=False,那么此时会抛出queue.Empty的错误
from multiprocessing import Process, Queue
from _queue import Empty
from queue import Full
import time
q = Queue()
def producer(q, names):
for name in names:
# 调用put方法推送数据
try:
q.put({"name": name}, block=False)
time.sleep(0.5)
except Full:
print("Queue is full")
def consumer(q):
while True:
try:
# 调用get方法获取数据
# 由于此方法会阻塞,所以设置超时时间,如果超时会抛出Empty异常
print(q.get(timeout=3))
except Empty:
break
if __name__ == '__main__':
names = ["Jack", "William", "Bill"]
pro = Process(target=producer, args=(q, names))
con = Process(target=consumer, args=(q,))
con.start()
pro.start()
pro.join()
con.join()
qsize()
返回队列的大致长度。由于多线程或者多进程的上下文,这个数字是不可靠的。
from multiprocessing import Process, Queue
from _queue import Empty
import time
q = Queue()
def producer(q, names):
for name in names:
q.put({"name": name})
time.sleep(0.5)
def consumer(q):
while True:
try:
time.sleep(1)
print(q.get(timeout=3))
except Empty:
break
def get_q_size(q):
while True:
print(q.qsize())
time.sleep(1)
if __name__ == '__main__':
names = ["Jack", "William", "Bill"]
con = Process(target=consumer, args=(q,))
pro = Process(target=producer, args=(q, names))
get_size = Process(target=get_q_size, args=(q,))
# 设置守护主进程,当主进程结束时则不再查询队列长度
get_size.daemon = True
get_size.start()
con.start()
pro.start()
pro.join()
con.join()
empty() and full()
empty()
如果队列是空的,返回 True ,反之返回 False 。 由于多线程或多进程的环境,该状态是不可靠的。
full()
如果队列是满的,返回 True ,反之返回 False 。 由于多线程或多进程的环境,该状态是不可靠的。
from multiprocessing import Process, Queue
from _queue import Empty
from queue import Full
import time
def producer(q, names):
while names:
if q.full():
time.sleep(0.5)
continue
q.put({"name": names.pop()}, block=False)
def consumer(q):
while True:
if q.empty():
time.sleep(2)
continue
print(q.get(timeout=3))
if __name__ == '__main__':
# 设置队列最大长度为2
q = Queue(maxsize=2)
names = ["Jack", "William", "Bill", "Mark", "Henry", "Vincent", "Jseph"]
con = Process(target=consumer, args=(q,))
pro = Process(target=producer, args=(q, names))
con.start()
pro.start()
pro.join()
con.join()
put_nowait(obj) 和 get_nowait()
这两个等同于以下操作,就不多细谈了。
put_nowait(obj) == put(obj, block=False)
get_nowait() == get(block=False)
close()
指示当前进程将不会再往队列中放入对象。一旦所有缓冲区中的数据被写入管道之后,后台的线程会退出。这个方法在队列被gc回收时会自动调用。
进程间共享状态
在进行并发编程时,通常最好尽量避免使用共享状态。使用多个进程时尤其如此。
但是,如果你真的需要使用一些共享数据,那么 multiprocessing 提供了共享内存、服务进程两种方法。
共享内存
可以使用 Value 或 Array 将数据存储在共享内存映射中。
Value
通过实例化一个Value对象,可以通过第一个参数指定数据类型,‘d’ 表示双精度浮点数, ‘i’ 表示有符号整数,调用实例对象value属性进行操作。
但多进程操作改属性时容易造成数据的污染,那么可以传入参数lock=True(默认为True), 此时可以使用递归锁get_lock()操作数据以保证数据的原子性。如果lock=False则无法使用递归锁,调用get_lock()时会抛出AttributeError: ‘c_long’ object has no attribute ‘get_lock’
from multiprocessing import Value
from multiprocessing import Process
import time
def set_num(num, increment):
for i in range(10):
# 如果不使用锁的话会导致num值计算错误
with num.get_lock():
print(f"set_num increment {increment} begin: ", num.value)
num.value += increment
print(f"set_num increment {increment} done: ", num.value)
time.sleep(1)
if __name__ == '__main__':
num = Value('d', 0.0)
p1 = Process(target=set_num, args=(num, 1))
p2 = Process(target=set_num, args=(num, 2))
p2.start()
p1.start()
p1.join()
p2.join()
Array
通过实例化一个Value对象,可以通过第一个参数指定数据类型,‘d’ 表示双精度浮点数, ‘i’ 表示有符号整数,调用实例对象下标进行操作。
from multiprocessing import Array
from multiprocessing import Process
def set_num(arr, increment):
for i in range(len(arr)):
with arr.get_lock():
arr[i] += increment
if __name__ == '__main__':
arr = Array('i', [1, 2, 3, 4])
p1 = Process(target=set_num, args=(arr, 1))
p1.start()
p1.join()
print(arr[:])
服务进程
由 Manager() 返回的管理器对象控制一个服务进程,该进程保存Python对象并允许其他进程使用代理操作它们。
Manager() 返回的管理器支持类型: list 、 dict 、 Namespace 、 Lock 、 RLock 、 Semaphore 、 BoundedSemaphore 、 Condition 、 Event 、 Barrier 、 Queue 、 Value 和 Array 。
Array
from multiprocessing import Process, Manager
def set_num(arr, increment):
for i in range(len(arr)):
arr[i] += increment
if __name__ == '__main__':
with Manager() as manager:
arr = manager.Array('i', [1, 2, 3, 4])
p1 = Process(target=set_num, args=(arr, 1))
p1.start()
p1.join()
print(arr)
list
from multiprocessing import Process, Manager
def set_num(m_list, increment):
m_list.append(increment)
if __name__ == '__main__':
with Manager() as manager:
m_list = manager.list([1, 2, 3, 4])
p1 = Process(target=set_num, args=(m_list, 1))
p1.start()
p1.join()
print(m_list)
Python 进程池
如果用户需要大批量的进行并发操作,因为频繁创建进程及销毁进程对资源的损耗是一笔不小的开销,使用进程池初始化后,进程池会根据用户指定的进程数量控制多个常驻的进程去执行用户的并发操作,这样可以有效的减少cpu创建及销毁进程的资源损耗,以及减少用户对于进程的操作,专注于业务即可。
apply和apply_async
apply(func[, args[, kwds]])
提供了阻塞式的任务执行功能,当该任务未执行完之前当前主进程会被阻塞住,直至任务完成后继续执行
apply_async(func[, args[, kwds[, callback[, error_callback]]]])
异步提交任务,可接受任务成功执行后回调函数callback参数,以及程序报错错误回调函数
error_callback参数,不会阻塞住主程序执行,并行任务推荐使用apply_async
如果指定了 callback , 它必须是一个接受单个参数的可调用对象。当执行成功时, callback 会被用于处理执行后的返回结果,否则,调用 error_callback 。
如果指定了 error_callback , 它必须是一个接受单个参数的可调用对象。当目标函数执行失败时, 会将抛出的异常对象作为参数传递给 error_callback 执行。
from multiprocessing import Pool
import time
import os
def init_func():
print("processes_init_function------>")
def pro_first(num):
print(num)
print("process pid", os.getpid())
time.sleep(1)
return num
def finish(res):
print(f"finish task {res}")
if __name__ == '__main__':
# 初始化进程池,processes指定进程多进程数量,如果不指定则默认为os.cpu_count(),也就是系统cpu数量
with Pool(processes=3, initializer=init_func) as pool:
for i in range(6):
pool.apply_async(pro_first, args=(i,), callback=finish) # 维持执行的进程总数为processes,当一个进程执行完毕后会添加新的进程进去
pool.close()
pool.join()