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

Python多进程写入共享字典速度过慢如何优化

Python多进程批量处理文档性能低于单核问题的排查与优化

问题场景

批量处理文档时,需将各文档的处理结果汇总存储,使用Python multiprocessing 模块实现多进程处理时,性能远低于单核串行方案:

  • 初始方案使用 Manager 创建跨进程共享字典写入结果,速度比单核方案慢27倍,实现代码如下:
def pooler(self, payload):
        lis_docs=[]
        for keys in payload.keys():
            lis_docs.append([keys,payload[keys]])

        # parallel process on this
        manager=mp.Manager()
        processed_docs=manager.dict()
        pool=mp.Pool(os.cpu_count()-1)
        for doc in lis_docs:
            pool.apply_async(parallel_process_for_shortq,args=(self,doc,processed_docs))
        
        pool.close()
        pool.join()

def parallel_process_for_shortq(self,doc,processed_doc):
        sentences=tokenize_sentences(doc[1])
        modified_text=" ".join(sentences)

        keywords=get_keywords(self.nlp,modified_text,4,self.s2v,self.fdist,self.normalized_levenshtein,len(sentences))
        keyword_sentence_mapping = get_sentences_for_keyword(keywords, sentences)

        processed_doc[doc[0]]=[keywords,keyword_sentence_mapping]

所有文档ID唯一,不存在多进程覆写同一条数据的问题,最初诉求为聚合多进程返回结果、统一写入字典,规避共享字典的性能损耗。

  • 实际测试性能数据:
    • 测试集仅3个文档时,单核串行总耗时1.76秒,apply_async+共享字典方案总耗时27.5秒
    • 放弃共享字典,改用starmap直接聚合子进程返回值后,耗时仍达到27秒,调整后代码如下:
def pooler(self, payload):
        lis_docs=[]
        for keys in payload.keys():
            lis_docs.append([keys,payload[keys]])

        manager=mp.Manager()
        n_cores=os.cpu_count()
        print(n_cores)
        start=time.time()

        with mp.Pool(n_cores-1) as pool:
            res=pool.starmap(parallel_process_for_shortq,zip(repeat(self),lis_docs))
        
        pool.close()
        pool.join()

def parallel_process_for_shortq(self,doc):
        sentences=tokenize_sentences(doc[1])
        modified_text=" ".join(sentences)

        keywords=get_keywords(self.nlp,modified_text,4,self.s2v,self.fdist,self.normalized_levenshtein,len(sentences))
        keyword_sentence_mapping = get_sentences_for_keyword(keywords, sentences)

        return [doc[0],keywords,keyword_sentence_mapping]
  • 测试集扩大到900个文档时性能有所提升,单文档平均耗时1.7秒,但仍远高于单核串行0.6秒/文档的速度。

问题根因

性能损耗和共享字典无直接关系,核心来自三类开销:

  • 进程启动固定开销:Python multiprocessing 默认spawn模式下,每个子进程启动需要重新导入依赖、初始化运行环境,3个文档的计算量完全覆盖不了进程启动成本,因此小批量任务下慢20余倍属于正常现象。
  • 大对象重复序列化开销:通过repeat(self)将类实例传入子进程时,实例上挂载的nlp、s2v、fdist等NLP大模型/数据对象,会在每次任务调度时经历pickle序列化、跨进程传输、反序列化流程,这部分开销远高于文档处理本身的计算量,直接抵消多进程的算力收益,是大批量任务下性能仍低于单核的核心原因。
  • 冗余开销:第二版代码中保留了未使用的mp.Manager()初始化,会额外启动独立管理进程,带来无意义的资源消耗。

优化方案

按优先级落地以下调整即可获得符合预期的多进程加速比:

  1. 移除无用的Manager初始化:采用进程返回值聚合结果的方案时,不需要启动Manager服务,直接删除相关代码即可。
  2. 避免大对象重复序列化传输:把NLP模型、s2v、fdist这类初始化一次即可复用的大对象,放到进程初始化阶段加载,不要通过任务参数传给子进程,参考实现如下:
# 定义进程内全局变量存复用的大对象
_worker_nlp = None
_worker_s2v = None
_worker_fdist = None
_worker_levenshtein = None

def _init_worker(nlp, s2v, fdist, normalized_levenshtein):
    # 每个进程启动时仅执行一次初始化,全局持有复用对象
    global _worker_nlp, _worker_s2v, _worker_fdist, _worker_levenshtein
    _worker_nlp = nlp
    _worker_s2v = s2v
    _worker_fdist = fdist
    _worker_levenshtein = normalized_levenshtein

def _parallel_worker(doc):
    # 子进程任务直接调用全局持有的对象,无需每次传参序列化
    doc_id, doc_content = doc
    sentences = tokenize_sentences(doc_content)
    modified_text = " ".join(sentences)
    keywords = get_keywords(
        _worker_nlp, modified_text, 4, _worker_s2v,
        _worker_fdist, _worker_levenshtein, len(sentences)
    )
    keyword_sentence_mapping = get_sentences_for_keyword(keywords, sentences)
    return (doc_id, [keywords, keyword_sentence_mapping])

# 主进程pooler方法调整
def pooler(self, payload):
    lis_docs = list(payload.items())
    n_cores = os.cpu_count() - 1
    # 初始化进程池时传入初始化函数和大对象,每个进程仅加载一次
    with mp.Pool(
        n_cores,
        initializer=_init_worker,
        initargs=(self.nlp, self.s2v, self.fdist, self.normalized_levenshtein)
    ) as pool:
        # 直接传入文档列表,设置合理chunksize减少IPC调度开销
        res = pool.map(_parallel_worker, lis_docs, chunksize=16)
    # 所有任务完成后一次性聚合为字典
    processed_docs = dict(res)
    return processed_docs
  1. 合理设置chunksize:处理千级文档量时,设置chunksize为16~64区间值,避免进程每次只取1个任务,减少跨进程调度开销。
  2. 小批量任务走串行逻辑:单批次文档量小于10个时,进程启动开销远大于计算收益,直接串行处理即可,无需启动多进程。

预期效果

落地以上调整后,千级文档量级的任务多进程加速比可以达到CPU核心数的70%~90%,不会再出现比单核慢的情况;10个文档以内的小批次走串行逻辑,可保持原有单核处理速度。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.09.01 23:15:41