Python 现代开发学习笔记

(五)进程池与独立的内存

1. 把一批计算交给子进程

现在任务改成计算 0² + 1² + ... + (n−1)²。这段循环主要占用 CPU,不在等网络。用多个进程执行独立计算:

# 项目文件:demos/process_pool.py
# spawn 从可导入模块启动子进程;任务函数放在模块顶层,建池动作放进入口判断。
from concurrent.futures import ProcessPoolExecutor
from multiprocessing import get_context


def sum_squares(limit):
    return sum(value * value for value in range(limit))


if __name__ == "__main__":
    with ProcessPoolExecutor(max_workers=2, mp_context=get_context("spawn")) as pool:
        results = list(pool.map(sum_squares, [5, 10, 20]))
    print(results)

输出 [30, 285, 2470]。这几个小输入方便手算,启动进程的开销可能比计算本身还大;扩大每批计算规模,再比较串行与进程池的实际经过时间。

spawn 会启动新的解释器并导入入口模块,if __name__ == "__main__" 让子进程导入时只建立函数定义,不再次创建整套进程池。把代码保存为 .py 文件运行,任务使用顶层函数,实参与返回结果经过序列化传递。进程池与启动方式

主进程提交可序列化的数据,子进程独立计算并把结果交回

2. 修改的是子进程收到的那份对象

# 项目文件:demos/process_copy.py
# 子进程收到独立的数据;返回值带回修改后的列表,父进程原列表不变。
from concurrent.futures import ProcessPoolExecutor
from multiprocessing import get_context


def add_tag(values):
    values.append("异步")
    return values


if __name__ == "__main__":
    tags = ["基础"]
    with ProcessPoolExecutor(max_workers=1, mp_context=get_context("spawn")) as pool:
        returned = pool.submit(add_tag, tags).result()
    print("父进程", tags)
    print("子进程返回", returned)

输出 父进程 ['基础'] 与 子进程返回 ['基础', '异步']。这与线程共享同一份列表不同。数据库连接、文件句柄和一大份对象也不能想当然地作为同一资源交给各个进程;本章的项目只把普通记录作为任务结果,连接由最后保存数据的调用层建立。

3. 用同一批计算核对进程开销

先用小输入核对平方和,再扩大循环次数,对同样三项任务分别计时。进程池这一轮把启动、传参和收尾也算进去:

# 项目文件:demos/bench_cpu.py
# 两种方式计算完全相同的输入;先比较结果,再观察各自的实际经过时间。
from concurrent.futures import ProcessPoolExecutor
from multiprocessing import get_context
from time import perf_counter


def sum_squares(limit):
    return sum(value * value for value in range(limit))


if __name__ == "__main__":
    limits = [1_000_000, 1_200_000, 1_400_000]
    started = perf_counter()
    serial = [sum_squares(limit) for limit in limits]
    serial_seconds = perf_counter() - started
    started = perf_counter()
    with ProcessPoolExecutor(max_workers=2, mp_context=get_context("spawn")) as pool:
        parallel = list(pool.map(sum_squares, limits))
    process_seconds = perf_counter() - started
    print("结果一致", serial == parallel)
    print(f"顺序计算 {serial_seconds:.3f} 秒")
    print(f"进程计算 {process_seconds:.3f} 秒")

第一行应为 结果一致 True,两行时间以本机实际运行为准。这批循环总共处理 360 万项,几乎一直在做整数运算;第一节的 sleep 和后面的 HTTP 请求则主要留出了等待外部操作的空隙。进程能够利用多个核心,但启动、数据传递和进程数量都会影响总时间。调整这批输入的规模,再比较变化,不能只看“用了进程”就判断一定更快。

笔记正文与全部题解 · Ctrl / ⌘ K