Python ProcessPoolExecutor未并行执行函数的原因排查
问题原因
你的代码没实现加速、看起来像串行,根本不是多进程没启动,是额外开销完全吃掉了并行收益,甚至倒贴性能,具体几个核心问题:
- 每帧重复创建销毁进程池:你把
with ProcessPoolExecutor()写在了帧处理的循环里,每读一帧就要重新走一遍进程启动、环境初始化、池销毁的流程。Windows/macOS默认的spawn进程启动模式下,每个子进程要重新加载Python解释器、OpenCV库、你的业务代码,单个进程启动开销就有几百毫秒,4个进程光启动就要花1秒以上,远超过光流计算本身的0.2秒耗时。 - OpenCV内部多线程和多进程冲突:OpenCV默认会开启多线程优化,
cv.calcOpticalFlowFarneback本身单进程就会吃满所有CPU核心,你再拆成4个进程跑,每个进程内部的OpenCV又会开多个线程,等于在4核CPU上跑十几个线程抢时间片,上下文切换开销直接拉满。 - 跨进程数据序列化开销大:你把拆分后的numpy图像块作为参数传给子进程时,Python会对数组做pickle序列化、跨进程拷贝、子进程反序列化,对于分辨率不高的图像块来说,这个传输开销比光流计算本身的耗时还高。
- 日志顺序误导判断:多进程的标准输出没有加锁,控制台打印日志的先后顺序完全不代表任务实际执行顺序。看你贴的时间戳,第二个任务启动时间2.075比第一个任务的2.105还早,四个任务实际是同时启动的,每个任务计算耗时只有30~40毫秒,根本不是串行执行。
修复方案
按优先级修改:
- 全局复用进程池:把进程池初始化放到程序启动阶段,不要放在帧循环里反复创建销毁,示例代码:
import concurrent.futures import cv2 import multiprocessing def ComputeDenseFlow(gray, prevgray, FlowMethod="Farneback", resx=720, resy=576): if FlowMethod=="Farneback": flow = cv.calcOpticalFlowFarneback(prevgray, gray, None, 0.5, 3, 15, 3, 5, 1.2, 0) elif FlowMethod=="DTVL1": optical_flow = cv.optflow.DualTVL1OpticalFlow_create(lambda_=0.15, theta=0.25, nscales=3, warps=4, epsilon=0.06) flow = optical_flow.calc(prevgray,gray, None) return flow if __name__ == "__main__": # 关闭OpenCV内部多线程,避免和多进程抢资源 cv2.setNumThreads(0) # Linux环境可开启fork模式大幅降低进程启动开销,Windows不支持 # multiprocessing.set_start_method('fork') # 程序启动时一次性初始化进程池,显式指定4个worker executor = concurrent.futures.ProcessPoolExecutor(max_workers=4) cam = cv2.VideoCapture(0) resx, resy = 720, 576 prevgray = None while True: _ret, img = cam.read() if not _ret: break resized=cv.resize(img,(resx,resy),interpolation=cv.INTER_AREA) gray = cv.cvtColor(resized, cv.COLOR_BGR2GRAY) if prevgray is not None: graylist = SplitIn4(gray,resx,resy) prevgraylist = SplitIn4(prevgray,resx,resy) # 复用已初始化的进程池提交任务,list()触发结果取回 flows = list(executor.map(ComputeDenseFlow, graylist, prevgraylist)) prevgray = gray # 程序退出前关闭进程池 executor.shutdown()
- 关闭OpenCV内部多线程:在导入cv2之后第一时间加
cv2.setNumThreads(0),禁止OpenCV在子进程里自动创建线程抢占CPU资源。 - 优化跨进程数据传输:如果还是觉得传输开销大,可以用
multiprocessing.shared_memory创建共享内存块存图像数据,子进程直接读取共享内存,避免numpy数组重复序列化拷贝。 - Linux环境改用fork启动模式:在main函数最开头加
multiprocessing.set_start_method('fork'),fork模式会直接复制父进程内存,不需要重新加载依赖库,进程启动开销比spawn小一个数量级。
额外提醒:OpenCV自带的Farneback、DTVL1光流本身已经做了高度的指令集和多线程优化,单进程跑0.2秒的情况下,拆成多进程跑叠加各类开销,大概率很难拿到明显的加速比,甚至可能更慢,可以先测完复用进程池后的实际耗时再决定要不要继续用多进程方案。
内容的提问来源于stack exchange,提问作者Vicente Garção
相关产品推荐
相关产品推荐

