You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

使用PyObjC与Apple Vision的VNRecognizeTextRequest出现内存泄漏

问题:Mac M1上Python多进程结合Apple Vision处理图片内存泄漏

我在Mac M1设备上用PyCharm开发,通过Python的multiprocessing库结合Apple Vision库提取约10000张PNG图片的文本时,发现内存持续增长直至失控。为排查问题,我简化流程用单张图片重复处理3000次,仍出现相同情况。已尝试使用NSAutoreleasePool但无效果。

测试代码如下:

import time
from Quartz import CIImage
from Vision import VNImageRequestHandler, VNRecognizeTextRequest
import multiprocessing
from Cocoa import NSURL, NSAutoreleasePool
import psutil
import os


def worker(job_queue):
    process = psutil.Process(os.getpid())
    while True:
        # Create an autorelease pool to manage memory
        pool = NSAutoreleasePool.alloc().init()

        try:
            t1 = time.time()
            job = job_queue.get()
            if job is None:  # Sentinel value to signal process to stop
                print(f"Worker {multiprocessing.current_process().name} exiting.")
                job_queue.task_done()
                memory_info = process.memory_info().rss
                print((multiprocessing.current_process().name, memory_info / 1024 / 1024))
                break
            print(f"Worker {multiprocessing.current_process().name} Job Number = {job[1]} url = {job[0]}")
            url = NSURL.fileURLWithPath_(job[0])
            ci_image = CIImage.imageWithContentsOfURL_(url)

            request = VNRecognizeTextRequest.alloc().init()
            handler = VNImageRequestHandler.alloc().initWithCIImage_options_(ci_image, None)
            success, error = handler.performRequests_error_([request], None)
            if error:
                print(f"Worker {multiprocessing.current_process().name} result = {success}, error = {error}")

            print(f"Worker {multiprocessing.current_process().name} done. Elapsed = {time.time() - t1}.")
            request = None
            handler = None
            # Get memory usage of the current process
            memory_info = process.memory_info().rss  # Resident Set Size in bytes

            print((multiprocessing.current_process().name, memory_info / 1024 / 1024))

        finally:
            # Ensure the autorelease pool is drained to release memory
            pool.drain()

        job_queue.task_done()

    memory_info = process.memory_info().rss
    print((multiprocessing.current_process().name, memory_info / 1024 / 1024))


if __name__ == '__main__':
    # Create a Queue for jobs
    job_queue = multiprocessing.JoinableQueue()

    # Number of worker processes
    num_workers = 4

    # Create worker processes
    processes = []
    for i in range(num_workers):
        p = multiprocessing.Process(target=worker, args=(job_queue,), name=f"Worker-{i + 1}")
        processes.append(p)
        p.start()

    # Put jobs into the queue
    num_jobs = 3000
    for job_id in range(num_jobs):
        job_queue.put(["Path To File", job_id])

    # Add sentinel values (None) to signal workers to stop after jobs are done
    for _ in range(num_workers):
        job_queue.put(None)

    # Wait for all jobs in the queue to be processed
    job_queue.join()

    # Wait for all worker processes to finish
    for p in processes:
        p.join()

    print("All jobs completed.")

程序启动4个进程,向队列加载3000个任务,每个进程从队列取任务执行文本识别后标记完成并取下一个任务。已移除文本识别结果处理部分,但内存仍会增长,每个进程内存从约250MB升至340MB。处理更多文件会出现内存耗尽问题,求解决方法。


解决方案

1. 显式释放Objective-C对象

Python的自动垃圾回收无法完全处理Objective-C对象的引用计数,需手动调用release()方法释放所有创建的Cocoa/Quartz/Vision对象,而非仅赋值为None。修改任务处理部分的代码:

# 在任务处理完成后添加
request.release()
handler.release()
ci_image.release()
url.release()
# 再置空对象
request = None
handler = None
ci_image = None
url = None

2. 调整NSAutoreleasePool作用范围

将job_queue.get()移出Autorelease Pool,避免队列操作相关对象被纳入池管理;仅在处理图片识别的逻辑块内创建池,确保所有Objective-C对象都在池的生命周期内:

def worker(job_queue):
    process = psutil.Process(os.getpid())
    while True:
        # 先取任务,再创建池
        job = job_queue.get()
        if job is None:
            print(f"Worker {multiprocessing.current_process().name} exiting.")
            job_queue.task_done()
            memory_info = process.memory_info().rss
            print((multiprocessing.current_process().name, memory_info / 1024 / 1024))
            break
        
        pool = NSAutoreleasePool.alloc().init()
        try:
            t1 = time.time()
            print(f"Worker {multiprocessing.current_process().name} Job Number = {job[1]} url = {job[0]}")
            url = NSURL.fileURLWithPath_(job[0])
            ci_image = CIImage.imageWithContentsOfURL_(url)

            request = VNRecognizeTextRequest.alloc().init()
            handler = VNImageRequestHandler.alloc().initWithCIImage_options_(ci_image, None)
            success, error = handler.performRequests_error_([request], None)
            if error:
                print(f"Worker {multiprocessing.current_process().name} result = {success}, error = {error}")

            print(f"Worker {multiprocessing.current_process().name} done. Elapsed = {time.time() - t1}.")
            
            # 显式释放对象
            request.release()
            handler.release()
            ci_image.release()
            url.release()
            request = None
            handler = None
            ci_image = None
            url = None
            
            memory_info = process.memory_info().rss
            print((multiprocessing.current_process().name, memory_info / 1024 / 1024))

        finally:
            pool.drain()
        
        job_queue.task_done()

3. 改用multiprocessing.Pool管理进程

手动管理进程容易出现资源累积,改用multiprocessing.Pool可以自动回收进程资源,避免长期运行的内存泄漏:

import gc
import time
from Quartz import CIImage
from Vision import VNImageRequestHandler, VNRecognizeTextRequest
import multiprocessing
from Cocoa import NSURL, NSAutoreleasePool
import psutil
import os

def process_image(job):
    path, job_id = job
    process = psutil.Process(os.getpid())
    pool = NSAutoreleasePool.alloc().init()
    try:
        t1 = time.time()
        print(f"Processing Job Number = {job_id}")
        url = NSURL.fileURLWithPath_(path)
        ci_image = CIImage.imageWithContentsOfURL_(url)

        request = VNRecognizeTextRequest.alloc().init()
        handler = VNImageRequestHandler.alloc().initWithCIImage_options_(ci_image, None)
        success, error = handler.performRequests_error_([request], None)
        if error:
            print(f"Job {job_id} result = {success}, error = {error}")

        print(f"Job {job_id} done. Elapsed = {time.time() - t1}.")
        
        # 释放对象
        request.release()
        handler.release()
        ci_image.release()
        url.release()
        
        # 手动触发GC
        gc.collect()
        
        memory_info = process.memory_info().rss
        print((f"Job {job_id}", memory_info / 1024 / 1024))
        return success, error
    finally:
        pool.drain()

if __name__ == '__main__':
    num_workers = 4
    num_jobs = 3000
    jobs = [("Path To File", job_id) for job_id in range(num_jobs)]
    
    with multiprocessing.Pool(num_workers) as pool:
        results = pool.map(process_image, jobs)
    
    print("All jobs completed.")

4. 手动触发Python垃圾回收

在每次任务处理完成后,调用gc.collect()强制回收Python层面的垃圾,配合Objective-C对象的手动释放,进一步降低内存累积风险。记得先导入gc模块。

5. 限制任务队列长度

若坚持使用手动队列管理,设置队列的maxsize参数(比如maxsize=num_workers*2),避免一次性加载大量任务,减少内存占用压力:

job_queue = multiprocessing.JoinableQueue(maxsize=num_workers*2)

内容的提问来源于stack exchange,提问作者Gyun

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.17 22:24:55