Python多进程与多线程并发执行多个.py文件实战指南

Python多进程与多线程并发执行多个.py文件实战指南

1. 项目概述:为什么我们需要同时执行多个.py文件?

在数据处理、自动化测试、爬虫或者后端服务开发中,我们经常会遇到一个场景:手头有一堆独立的Python脚本(.py文件),它们各自负责一项具体的任务。比如,一个脚本负责从API拉取数据,另一个脚本负责清洗数据,还有一个脚本负责将结果写入数据库。最直接的做法是,在命令行里一个接一个地手动执行,或者写一个简单的批处理脚本按顺序调用。但这样做效率太低了,尤其是当某些脚本执行时间很长,或者它们之间没有依赖关系时,宝贵的计算资源和时间就在“等待”中被白白浪费了。

这就是“多个py文件同时执行”要解决的核心痛点:提升任务的整体吞吐量和执行效率。想象一下,你有一个四核的CPU,却只用一个核心在单线程地跑任务,其他三个核心都在“围观”,这显然不是现代计算机该有的工作方式。我们的目标,就是让这些独立的.py文件能够“齐头并进”,充分利用系统资源。

实现这一目标,主要有两大技术路径:多进程(Multiprocessing)多线程(Multithreading)。虽然最终目的都是“同时干多件事”,但它们的底层原理、适用场景和注意事项截然不同。选择哪条路,直接决定了你程序的性能表现和稳定性。很多人刚开始接触时容易混淆,用错了场景,结果可能比单线程还要慢,或者出现各种诡异的bug。

所以,这篇内容不是简单地扔给你几行代码,而是会深入拆解这两种并发模型在Python中的具体实现,从原理到实践,从代码到踩坑经验,让你彻底搞清楚:面对一堆待执行的.py文件,你究竟该用多进程还是多线程?具体每一步该怎么操作?过程中会遇到哪些“坑”,又该如何避开?

2. 核心思路与方案选型:多进程 vs 多线程

在动手写代码之前,我们必须先做出最重要的架构决策:选择多进程还是多线程。这个选择没有绝对的对错,只有是否适合你的具体场景。理解它们的区别,是写出高效、稳定并发程序的第一步。

2.1 根本差异:GIL与内存空间

Python(特指CPython解释器)有一个著名的“全局解释器锁”(GIL)。简单来说,GIL保证了同一时刻只有一个线程可以执行Python字节码。这意味着,即使在多核CPU上,纯Python代码的多线程也无法实现真正的并行计算,线程们需要排队获取这把“锁”才能执行。所以,Python的多线程对于CPU密集型任务(如科学计算、图像处理)是无效的,它无法利用多核优势。

多进程则彻底绕过了GIL的限制。每个进程都有自己独立的Python解释器和内存空间,因此多个进程可以在不同的CPU核心上真正并行运行。这是处理CPU密集型任务的利器。

但是,多进程的“独立”也带来了代价:进程间通信(IPC)比线程间通信要复杂和昂贵得多。线程共享同一进程的内存,数据交换非常快;而进程之间内存不共享,需要通过队列(Queue)、管道(Pipe)或者共享内存等机制来传递数据,这会引入额外的开销。

2.2 方案选型决策表

为了帮你快速决策,我整理了下面这个表格:

特性维度多进程 (Multiprocessing)多线程 (Multithreading)
并行能力真正并行,可利用多核CPU。并发而非真并行(受GIL限制),适合I/O等待。
内存隔离内存完全独立,一个进程崩溃不影响其他。共享同一进程内存,一个线程崩溃可能导致整个进程崩溃。
创建开销大(需复制父进程资源)。小(共享进程资源)。
通信开销大(需IPC机制)。小(直接读写共享变量即可)。
数据共享复杂,需使用multiprocessing.Manager、队列等。简单,但需注意线程安全(使用锁Lock)。
适用场景CPU密集型计算(如数值模拟、数据加密、图像渲染)。I/O密集型任务(如网络请求、磁盘读写、数据库查询)。
风险资源消耗大,进程管理稍复杂。线程安全风险(竞态条件),调试困难。

如何应用到我们的“多个.py文件”场景?

  • 如果你的这些.py文件主要是做大量的数学计算、数据转换等消耗CPU的操作,那么请选择多进程。例如,同时运行多个机器学习模型训练脚本。
  • 如果你的这些.py文件主要是访问网络API、读写文件或数据库等大部分时间在等待外部响应的操作,那么多线程是更轻量、更高效的选择。例如,同时运行多个爬虫脚本去抓取不同网站的数据。

注意:这里讨论的是执行独立的.py文件,即每个文件都是一个完整的、可独立运行的程序。这与在一个.py文件内部定义多个函数,然后用并发技术调用这些函数,在思路上是相通的,但入口点不同。我们的核心思路是:由一个“主调度程序”来负责启动和管理这些独立的子任务。

2.3 第三种思路:进程池与线程池

无论是多进程还是多线程,直接创建大量进程/线程都是危险的,会消耗巨量资源,可能导致系统崩溃。更优雅和专业的做法是使用池(Pool)

池的概念就像一家公司的固定团队编制。你有一个任务队列(一堆.py文件),但你不必为每个任务都招聘(创建)一个新员工(进程/线程),然后任务做完就开除(销毁)。你可以维护一个固定大小的团队(池),有新的任务来了,就从池子里找一个空闲的员工去处理。处理完了,员工回来等待下一个任务。

这样做的好处是:

  1. 资源可控:避免了创建和销毁进程/线程的巨大开销。
  2. 管理方便:池会帮你处理任务分配、结果收集等繁琐工作。
  3. 性能稳定:防止系统因进程/线程数量爆炸而过载。

在接下来的实操中,我们将分别使用multiprocessing.Poolconcurrent.futures.ThreadPoolExecutor来实现进程池和线程池,这是生产环境中推荐的做法。

3. 多进程方案实现:用进程池并行执行CPU密集型脚本

假设我们有三个CPU密集型的脚本:calc_square.py(计算平方)、calc_cube.py(计算立方)和calc_factorial.py(计算阶乘)。它们的内容很简单,但模拟了耗时的计算。

3.1 子任务脚本示例

calc_square.py

# calc_square.py import time def main(): print(f"[Square] 进程 {os.getpid()} 开始运行") result = [i ** 2 for i in range(1000000)] # 模拟计算 time.sleep(2) # 模拟耗时 print(f"[Square] 进程 {os.getpid()} 运行结束") return len(result) # 返回一个结果示例 if __name__ == '__main__': import os main()

calc_cube.pycalc_factorial.py结构类似,只是计算逻辑和模拟时间不同。

3.2 主调度程序:multiprocessing.Pool

我们创建一个主程序master_multiprocessing.py来管理这些子脚本。

# master_multiprocessing.py import os import sys import subprocess from multiprocessing import Pool import time def run_script(script_path): """ 定义一个函数,用于在子进程中运行指定的Python脚本。 参数 script_path: 要执行的.py文件的路径。 返回: (脚本路径, 退出码, 输出) """ print(f"启动进程执行脚本: {script_path} (PID: {os.getpid()})") start_time = time.time() try: # 使用subprocess.run来执行外部脚本,并捕获输出 # 设置`capture_output=True`以捕获标准输出和错误 # `text=True`让返回的输出是字符串而非字节 result = subprocess.run( [sys.executable, script_path], # sys.executable 确保使用当前Python解释器 capture_output=True, text=True, timeout=30 # 设置超时,防止脚本卡死 ) elapsed = time.time() - start_time # 打印脚本的输出 if result.stdout: print(f"输出 [{script_path}]:\n{result.stdout.strip()}") if result.stderr: print(f"错误 [{script_path}]:\n{result.stderr.strip()}") print(f"脚本 {script_path} 执行完毕,耗时 {elapsed:.2f}秒,退出码: {result.returncode}") return (script_path, result.returncode, result.stdout, elapsed) except subprocess.TimeoutExpired: elapsed = time.time() - start_time print(f"警告: 脚本 {script_path} 执行超时 (>{30}秒),已终止。") return (script_path, -1, "Timeout", elapsed) except Exception as e: elapsed = time.time() - start_time print(f"执行脚本 {script_path} 时发生异常: {e}") return (script_path, -2, str(e), elapsed) if __name__ == '__main__': # 1. 定义要并行执行的所有.py文件路径列表 scripts_to_run = [ './calc_square.py', './calc_cube.py', './calc_factorial.py', # 可以继续添加更多脚本 ] print("开始使用多进程池执行任务...") pool_start = time.time() # 2. 创建进程池。进程数通常设置为CPU核心数,这里是4。 # 如果脚本都是CPU密集型,进程数最好等于或略小于CPU核心数。 cpu_count = os.cpu_count() pool_size = min(cpu_count, len(scripts_to_run)) if cpu_count else 4 print(f"系统CPU核心数: {cpu_count}, 设置进程池大小: {pool_size}") # 3. 使用进程池的map方法,将任务函数和参数列表映射到各个进程。 # `map`会阻塞,直到所有任务完成。 with Pool(processes=pool_size) as pool: results = pool.map(run_script, scripts_to_run) total_time = time.time() - pool_start print(f"\n所有任务执行完成!总耗时: {total_time:.2f}秒") print("\n=== 任务执行结果汇总 ===") for script, retcode, output, elapsed in results: status = "成功" if retcode == 0 else f"失败(码:{retcode})" print(f"脚本: {script:30} 状态: {status:15} 耗时: {elapsed:.2f}秒")

3.3 关键代码解析与实操要点

  1. 为什么用subprocess.run而不是直接import我们的目标是执行独立的.py文件,这些文件可能本身就是完整的程序,有它们自己的if __name__ == '__main__'入口。使用subprocess模块是最标准、最干净的方式,它会在一个全新的Python解释器进程中运行目标脚本,完全模拟了手动在命令行执行python script.py的效果,环境隔离性最好。

  2. sys.executable的重要性: 这确保了子进程使用与主程序完全相同的Python解释器路径。这能避免因为系统中有多个Python版本(如Python2和Python3,或系统Python与虚拟环境Python)而导致的版本冲突问题。这是一个非常实用的细节。

  3. 进程池大小pool_size的设置: 这是性能调优的关键。对于纯CPU密集型任务,理想情况是一个进程绑定一个CPU核心os.cpu_count()获取逻辑核心数。设置pool_size = cpu_count可以最大化利用CPU。如果任务数少于核心数,则以任务数为准。如果任务有I/O等待,可以适当调大池大小,但不宜过大,否则进程切换开销会抵消并发收益。

  4. pool.mapvspool.apply_asyncmap是同步的,它会等待所有任务完成才返回结果列表,代码简洁。apply_async是异步的,它提交任务后立即返回一个AsyncResult对象,不阻塞主程序,适合需要实时处理结果或执行超长任务的场景。对于我们这种“启动所有任务并等待全部完成”的批处理场景,map更合适。

实操心得:在Windows系统上使用multiprocessing时,必须将主程序的入口代码放在if __name__ == '__main__':之下。这是因为Windows没有fork系统调用,创建新进程时会重新导入主模块,如果没有这个保护,会导致无限递归创建子进程。这是一个经典的“坑”。

4. 多线程方案实现:用线程池并发执行I/O密集型脚本

现在,假设我们的脚本是I/O密集型的,例如从不同的网站API获取数据。我们创建三个模拟脚本。

4.1 子任务脚本示例(I/O密集型)

fetch_data_a.py

# fetch_data_a.py import time import random def main(): print(f"[Fetch A] 线程任务开始") # 模拟网络请求延迟,大部分时间在等待 delay = random.uniform(1, 3) # 1到3秒的随机延迟 time.sleep(delay) # 模拟获取到一些数据 data = {"source": "API_A", "items": [1, 2, 3]} print(f"[Fetch A] 获取数据完成,耗时 {delay:.2f}秒") return data if __name__ == '__main__': main()

fetch_data_b.pyfetch_data_c.py类似。

4.2 主调度程序:concurrent.futures.ThreadPoolExecutor

Python 3.2引入了concurrent.futures模块,它提供了更高级别的线程池和进程池接口,使用起来比原始的threadingmultiprocessing更简洁。我们使用ThreadPoolExecutor

# master_threading.py import concurrent.futures import subprocess import sys import os import time from typing import List def run_script_with_thread(script_path): """在线程中运行外部Python脚本""" print(f"线程启动,执行脚本: {script_path}") start = time.time() try: result = subprocess.run( [sys.executable, script_path], capture_output=True, text=True, timeout=10 # I/O任务超时时间可以设长一些 ) elapsed = time.time() - start output = result.stdout.strip() if result.stdout else "" if output: print(f"输出 [{os.path.basename(script_path)}]: {output}") return { "script": script_path, "returncode": result.returncode, "output": output, "error": result.stderr, "time": elapsed } except subprocess.TimeoutExpired: elapsed = time.time() - start print(f"超时: {script_path}") return {"script": script_path, "returncode": -1, "error": "Timeout", "time": elapsed} except Exception as e: elapsed = time.time() - start print(f"异常: {script_path} - {e}") return {"script": script_path, "returncode": -2, "error": str(e), "time": elapsed} if __name__ == '__main__': io_scripts = [ './fetch_data_a.py', './fetch_data_b.py', './fetch_data_c.py', ] print("开始使用线程池并发执行I/O密集型脚本...") # 设置线程池的最大工作线程数。 # 对于I/O密集型任务,线程数可以远大于CPU核心数,因为线程大部分时间在等待。 # 但也不是无限大,受限于系统资源(如网络连接数、内存)。通常从10-100开始测试。 max_workers = 10 all_results = [] start_total = time.time() # 使用ThreadPoolExecutor上下文管理器 with concurrent.futures.ThreadPoolExecutor(max_workers=max_workers) as executor: # 使用submit提交单个任务,返回Future对象 future_to_script = {executor.submit(run_script_with_thread, script): script for script in io_scripts} # 使用as_completed迭代已完成的任务,结果完成一个就处理一个 for future in concurrent.futures.as_completed(future_to_script): script = future_to_script[future] try: result = future.result(timeout=12) # 略大于单个任务超时时间 all_results.append(result) status = "成功" if result['returncode'] == 0 else "失败" print(f"任务完成: {script} -> {status} ({result['time']:.2f}s)") except concurrent.futures.TimeoutError: print(f"错误: 获取任务 {script} 结果超时") except Exception as exc: print(f"任务 {script} 生成异常: {exc}") total_elapsed = time.time() - start_total print(f"\n所有并发任务执行完毕。总耗时: {total_elapsed:.2f}秒") print("\n=== 详细结果 ===") for res in all_results: print(f"{res['script']}: 状态码{res['returncode']}, 耗时{res['time']:.2f}s")

4.3 关键代码解析与实操要点

  1. ThreadPoolExecutor的优势: 相比直接使用threading.ThreadThreadPoolExecutor提供了更现代、更易用的API。它自动管理线程的生命周期和任务队列,我们只需要关心提交任务(submit)和获取结果(as_completedmap)。

  2. max_workers线程数设置: 这是I/O密集型任务调优的核心。原则是:线程数 ≈ (I/O等待时间 / CPU处理时间) * CPU核心数。但由于等待时间通常很难精确估算,一个实用的经验法则是:从一个小数字(如10)开始,通过压力测试观察系统负载(CPU、内存、网络)和任务完成时间,逐步增加,直到性能不再提升或系统资源出现瓶颈。对于简单的网络请求,设置几十到几百都是常见的。

  3. as_completedmap的选择: 本例使用了executor.submit()配合concurrent.futures.as_completed()submit用于提交单个任务并获得一个Future对象。as_completed会生成一个迭代器,在任务完成时立即产出结果,无论任务提交的顺序如何。这非常有用,因为I/O任务完成时间不确定,我们可以先处理先完成的任务,实现更快的响应。如果希望严格按照提交顺序获取结果,则使用executor.map

  4. 线程安全与subprocess: 注意,我们在每个线程内部调用subprocess.run,这本身是线程安全的,因为subprocess模块会为每个调用创建独立的子进程。但是,如果多个线程需要读写同一个文件或共享变量,就必须引入锁(threading.Lock)来保证数据一致性。在我们的场景中,每个脚本独立运行,没有共享资源,所以无需考虑此问题。

注意事项:虽然GIL对I/O操作影响不大(因为线程在等待I/O时会释放GIL),但如果你的脚本中混有大量的CPU计算,多线程的性能提升会非常有限,甚至因为线程切换开销而变慢。此时,应考虑使用concurrent.futures.ProcessPoolExecutor(进程池)来替代线程池。

5. 进阶技巧与生产环境考量

将多个.py文件并发执行应用到实际生产环境,还需要考虑更多因素。下面分享一些从实战中总结的进阶技巧。

5.1 动态任务生成与依赖管理

很多时候,要执行的脚本列表不是静态的,可能根据配置文件、数据库查询或上游任务的结果动态生成。

# 示例:从配置文件或目录扫描动态获取任务列表 import glob import json def discover_scripts(config_path='task_config.json'): """从配置文件或目录发现需要执行的脚本""" # 方式1:从JSON配置文件读取 # with open(config_path, 'r') as f: # config = json.load(f) # scripts = config['scripts_to_run'] # 方式2:扫描特定目录下的所有.py文件(排除主程序本身) scripts = [] for file in glob.glob('./tasks/*.py'): if not file.endswith('__init__.py') and 'master' not in file: scripts.append(file) # 可以在这里根据脚本元信息(如优先级、依赖)排序 return sorted(scripts) # 简单按文件名排序

对于有依赖关系的任务(例如,B脚本需要A脚本的输出),简单的并发就不够了。你需要引入有向无环图(DAG)调度。虽然可以自己实现,但更推荐使用成熟的框架,如Apache AirflowCelery,它们专门为复杂的工作流设计,提供了依赖管理、任务重试、监控告警等全套功能。

5.2 超时、重试与优雅终止

网络不稳定、资源竞争都可能导致任务失败。一个健壮的系统必须具备容错机制。

  • 超时控制:如上文代码所示,在subprocess.run中设置timeout参数至关重要,防止某个脚本卡死拖垮整个任务流。
  • 重试机制:对于可能因临时网络抖动失败的任务,可以加入重试逻辑。
from tenacity import retry, stop_after_attempt, wait_exponential @retry(stop=stop_after_attempt(3), wait=wait_exponential(multiplier=1, min=2, max=10)) def run_script_with_retry(script_path): """使用tenacity库实现带指数退避的重试""" # ... subprocess.run 逻辑 ... if result.returncode != 0: raise Exception(f"Script failed with code {result.returncode}") return result
  • 优雅终止:当主程序收到终止信号(如Ctrl+C)时,应该通知所有子进程/线程,让它们完成当前工作或清理资源后再退出,而不是强制杀掉。这可以通过设置信号处理器和检查全局标志位来实现。

5.3 结果收集、日志与监控

当任务并发执行时,将各个任务的输出(日志、结果)集中管理非常重要。

  1. 结构化结果收集:我们的示例代码将每个任务的结果(退出码、输出、耗时)收集到一个列表里。在生产环境中,你可能需要将结果写入数据库(如SQLite、MySQL)、消息队列(如Redis)或文件(如JSON Lines格式),便于后续分析和追溯。
  2. 集中式日志:每个子进程/线程打印到各自的标准输出会混在一起,难以阅读。建议使用Python的logging模块,为每个任务配置独立的日志处理器(FileHandler),或者将所有日志发送到统一的日志收集系统(如ELK Stack)。
  3. 进度可视化:对于长时间运行的批处理任务,提供一个进度条能极大提升用户体验。可以使用tqdm库。
from tqdm import tqdm with concurrent.futures.ThreadPoolExecutor(max_workers=10) as executor: futures = {executor.submit(task, arg): arg for arg in task_list} # 使用tqdm包装as_completed for future in tqdm(concurrent.futures.as_completed(futures), total=len(futures)): result = future.result() # ... 处理结果 ...

5.4 资源限制与队列控制

无限制地提交任务可能导致内存溢出或把下游服务打挂。一个常见的模式是使用生产者-消费者模型配合有界队列

concurrent.futures.ThreadPoolExecutor内部已经有一个任务队列。你可以通过观察executor._work_queue.qsize()(注意这是内部属性,不稳定)或自定义一个计数器来监控队列积压。更高级的做法是使用asyncio的信号量(Semaphore)或第三方库如celery来精确控制并发度。

对于进程池,要特别注意内存使用。如果每个子进程都加载一个巨大的机器学习模型,那么创建多个进程会迅速吃光内存。这时可以考虑使用“惰性加载”或在进程间共享只读数据(通过multiprocessing.shared_memorymultiprocessing.Manager)。

6. 常见问题与排查技巧实录

在实际操作中,你肯定会遇到各种各样的问题。下面是我总结的一些典型问题及其解决方法。

6.1 问题排查速查表

现象可能原因排查步骤与解决方案
多进程程序在Windows上无限创建子进程未将主程序入口放在if __name__ == '__main__':下。严格检查主程序代码结构,确保进程启动代码在if __name__ == '__main__':块内。
多线程程序速度没提升,甚至更慢1. 任务本质是CPU密集型,受GIL限制。
2. 线程数设置过多,切换开销过大。
3. 存在全局锁竞争(如频繁写日志到同一文件)。
1. 使用top或任务管理器查看CPU使用率。若单个核心满载,其他空闲,则是GIL问题,换用多进程。
2. 降低线程数,进行性能压测,找到最优值。
3. 使用队列(queue.Queue)或线程安全的日志处理器。
子进程/脚本执行后无输出,或输出混乱1. 输出被缓冲,未及时刷新。
2. 多个进程/线程同时向标准输出打印,内容交织。
1. 在子脚本中,使用print(..., flush=True),或在执行时设置环境变量PYTHONUNBUFFERED=1
2. 主程序使用subprocess.run(capture_output=True)捕获输出后再统一打印。或为每个任务输出添加唯一前缀(如进程PID)。
出现PicklingError或序列化错误在跨进程传递参数或返回值时,对象无法被pickle模块序列化。确保传递给pool.map的函数参数和返回值都是可序列化的基本类型(int, str, list, dict等)或可pickle的自定义类。避免传递lambda函数、数据库连接等复杂对象。
任务执行一半莫名挂起,不报错也不结束1. 死锁(多线程中尤其常见)。
2. 子进程在等待永远不会到来的输入。
3. 资源耗尽(如文件描述符用尽)。
1. 使用threading的调试工具,或简化代码逻辑,避免嵌套锁。
2. 检查子脚本逻辑,确保没有input()或等待标准输入。
3. 使用ulimit -n检查并增加系统文件描述符限制。确保代码中正确关闭文件、网络连接。
内存使用量不断增长内存泄漏。多进程中,子进程结束后资源未释放;或多线程中,全局列表不断追加数据。1. 对于进程池,使用with Pool() as pool:确保池被正确关闭清理。
2. 定期清理全局缓存或使用弱引用。
3. 使用tracemallocobjgraph工具定位内存泄漏点。

6.2 一个真实的调试案例:日志文件被重复写入

我曾经遇到一个bug:使用多进程处理日志文件时,发现文件内容错乱,有些行丢失,有些行重复。原因是多个进程同时以追加模式(‘a’)打开了同一个日志文件并写入。虽然操作系统保证了单次写入的原子性,但多个进程的write操作交织在一起,导致内容混乱。

解决方案

  1. 每个进程写自己的日志文件:这是最彻底的方法,通过进程ID或任务ID来命名日志文件,例如app.log.pid_12345。最后再用一个工具合并日志。
  2. 使用日志服务:所有进程将日志发送到一个独立的日志进程(通过multiprocessing.Queue)或网络日志收集器(如syslog,logstash)。
  3. 使用进程安全的日志处理器:Python标准库的logging模块提供了QueueHandlerQueueListener,可以很方便地实现多进程安全日志。这是我最推荐的方法。
# 主进程中设置 import logging import logging.handlers from multiprocessing import Queue def logger_init(): log_queue = Queue() # 设置QueueListener,从队列取日志并交给真正的Handler处理 handler = logging.FileHandler('app.log') listener = logging.handlers.QueueListener(log_queue, handler) listener.start() # 返回一个配置好的logger,它使用QueueHandler logger = logging.getLogger('app') logger.addHandler(logging.handlers.QueueHandler(log_queue)) logger.setLevel(logging.INFO) return logger, listener # 子进程中,直接获取这个logger即可,无需额外配置 # logger = logging.getLogger('app') # logger.info('This is safe from multiple processes')

6.3 性能优化小技巧

  • 预热:对于需要加载大型模型或建立连接池的任务,可以在进程/线程创建后、正式处理任务前,先执行一次预热操作,避免第一次任务执行时间异常长。
  • 批处理:如果每个脚本任务都很小(例如处理一条数据),那么频繁创建子进程的开销会占主导。考虑将多个小任务打包成一个“批次”,交给一个脚本处理,变相增大任务粒度。
  • 监控:使用psutil库在运行时监控主进程和子进程的CPU、内存占用,便于发现性能瓶颈和内存泄漏。

最后,选择多进程还是多线程,以及如何配置参数,并没有银弹。最好的方法是在一个与生产环境相似的测试环境中,用真实的脚本和数据,进行不同配置下的压力测试和性能剖析,用数据来指导你的决策。我自己在项目中,通常会先实现一个可配置的版本,通过命令行参数来切换进程/线程模式、调整池大小,然后运行基准测试,找到那个性价比最高的“甜蜜点”。