Python从入门到实战(十六):多进程编程
目录一、 为什么需要并发1. 顺序执行2. 并发与并行3. 同步与异步4. 为什么需要多进程二、进程基础1. 什么是进程2. 主进程与子进程3. Process 类4. 创建与启动进程5. 为什么必须写 if __name__ __main__三、进程控制1. 进程生命周期 API1.1 进程启动start()1.2 进程等待与同步join(timeoutNone)1.3 进程状态查询is_alive()1.4 守护进程daemon 属性1.5 终止terminate() 与 kill()1.6 退出码exitcode 属性2. 生命周期控制示例四、进程同步1. 为什么需要锁2. Lock3. 获取锁与释放锁4. 标准规范5. 注意事项五、进程通信1. 为什么不能共享变量2. Queue3. Queue 使用案例4. Pipe5. Pipe 使用案例六、自定义 Process1. 继承 Process 类2. start() 与 run()3. 使用案例七、进程池1. 为什么需要进程池2. Pool 类3. 进程池常见 API4. 进程池生命周期5. 使用案例总结一、 为什么需要并发在前面的章节中我们编写的所有程序基本都是按单线程、顺序执行的方式运行的。在早期单核 CPU 时代这种线性思维符合计算机执行指令的逻辑然而现代计算机早已普及多核 CPU 架构。如果继续采用单线程顺序执行程序在面对计算密集型或高延迟 I/O 任务时就无法有效利用硬件资源。为了打破这一瓶颈我们需要引入多任务并发与并行编程在深入 Python 的 multiprocessing 模块之前我们必须先厘清并发编程中最基础的几个核心概念顺序执行、并发、并行、同步与异步1. 顺序执行顺序执行是最直观的程序运行方式。系统按照代码的先后顺序将任务逐个放入 CPU 中执行。前一个任务未完成前后一个任务必须处于等待状态特点简单直观逻辑可预测痛点若其中某个任务涉及高延迟操作CPU 就会进入空闲等待状态导致后续任务被阻塞整体吞吐量低下2. 并发与并行很多人常将“并发”与“并行”混为一谈但它们在计算机底层机制上有本质的区别并发并发是指系统具备处理多个任务的能力但并不意味着这些任务在同一时刻被执行在单核 CPU或 CPU 核心数少于任务数的情况下操作系统通过时间片轮转和上下文切换机制在极短的时间内频繁在多个任务之间交替切换。由于切换速度极快通常为毫秒级在宏观视角上给用户造成了 多个任务在同时运行 的错觉并行并行是指系统具备同时执行多个任务的能力这必须依赖多核 CPU。每个 CPU 核心在同一物理时刻各自独立运行一个任务任务之间在时间轴上是真正的重叠运行互不抢占 CPU 资源对比并发是关于 结构 的强调的是程序的设计结构能够处理多个事情并行是关于 执行 的强调的是硬件在同一时刻真正做多个事情3. 同步与异步这两个概念关注的是消息通信与控制权回收的机制同步调用方发起一个函数或任务请求后必须停下手头的所有工作等待该请求返回结果。在结果返回之前调用方处于阻塞状态无法继续执行后续代码异步调用方发起一个请求后无需等待结果返回即可立刻继续向下执行其他任务。被调用的任务会在后台独立运行当其处理完毕后通过回调函数、事件通知或信号通知调用方4. 为什么需要多进程在许多编程语言如 Java、C中提高并发/并行效率的首选方案是多线程但在 Python具体指最常用的 CPython 解释器中由于存在全局解释器锁GIL同一时刻只允许一个线程执行 Python 字节码。这就导致 Python 的多线程在 CPU 密集型任务中无法利用多核 CPU 的并行能力因此在 Python 中要实现真正的多核物理并行最主流且官方推荐的解决方案就是使用多进程——通过创建独立的 Python 进程让每个进程拥有自己独立的解释器与内存空间从而完美绕过 GIL 的限制二、进程基础在理解了并发与并行的区别后本章将正式切入 Python 的多进程核心模块——multiprocessing。从进程的概念出发剖析主进程与子进程的衍生关系并掌握 Process 类的基本使用规范1. 什么是进程在操作系统中进程是计算机进行资源分配和调度的基本单位当运行一个 Python 脚本时操作系统会为该程序开辟一块独立的内存空间并分配唯一的进程标识符进程特征独立的内存空间每个进程拥有自己独立的虚拟内存地址空间包含堆、栈、全局变量与代码段。不同进程之间无法直接共享内存数据独立的系统资源每个进程拥有独占的文件描述符表、CPU 寄存器状态以及上下文信息开销相对较大相比于线程创建、销毁或切换进程需要操作系统重新分配内存与上下文因此开销较高2. 主进程与子进程在多进程编程中进程之间存在明确的父子层级关系主进程启动 Python 脚本时创建的进程。主进程负责协调控制流、衍生子进程并回收资源子进程由主进程通过 multiprocessing.Process 显式派生出的新进程。子进程会继承父进程的部分环境但会在自己独立的空间中运行指定的任务代码PID 与 PPID使用 os.getpid() 可以获取当前进程的 PID使用 os.getppid() 可以获取父进程的 PID3. Process 类Python 提供了标准库 multiprocessing 中的 Process 类用于创建和管理子进程构造函数签名from multiprocessing import Process Process(groupNone, targetNone, nameNone, args(), kwargs{}, *, daemonNone) 参数解析 target:Callable —— 子进程启动后将要执行的函数/可调用对象 args:tuple —— 传递给 target 函数的位置参数元组 kwargs:dict —— 传递给 target 函数的关键字参数字典 name:str —— 子进程的自定义名称 daemon:bool —— 设置是否为守护进程4. 创建与启动进程通过一段代码演示如何使用 Process 创建并运行两个子进程import os import time from multiprocessing import Process def task(task_name: str, duration: int): 子进程执行的目标任务 print(f[子进程 {name}] 开始运行 | PID: {os.getpid()} | 父进程 PPID: {os.getppid()}) time.sleep(duration) print(f[子进程 {name}] 任务完成 | 耗时: {duration}s) if __name__ __main__: print(f[主进程] | PID: {os.getpid()}) # 1. 实例化 Process 对象此时仅分配了对象未真正创建进程 p1 Process(targettask, args(A, 2), nameWorker-A) p2 Process(targettask, kwargs{task_name: B, duration: 1}, nameWorker-B) # 2. 调用 start()向操作系统发起创建进程请求异步启动子进程 p1.start() p2.start()输出结果5. 为什么必须写 if __name__ __main__在 Python 多进程编程中如果不写 if __name__ __main__程序在 Windows 或 macOS系统下会抛出 RuntimeError导致无限递归创建子进程直至系统崩溃原因Windows / macOS (spawn 模式)创建子进程时操作系统并不会完全复制内存而是启动一个新的 Python 解释器进程然后以导入的方式重新加载主模块如果不加入口保护当子进程重新导入当前脚本文件时顶级作用域中的 p1 Process(...) 和 p1.start() 代码会被重新执行一遍子进程又尝试创建孙子进程造成无限递归循环加上入口保护在导入模块时 __name__ 的值为模块名而非 __main__因此子进程导入时不会重复触发进程创建逻辑三、进程控制实例化一个 Process 对象仅仅是在内存中创建了一个 Python 对象并不意味着已经开始运行。要精细化掌控进程的整个运行轨迹我们需要掌握其生命周期控制 API1. 进程生命周期 API1.1 进程启动start()机制向操作系统内核发起创建进程的系统调用。操作系统完成 PCB进程控制块分配、内存空间映射后隐式触发并执行 run() 方法特性非阻塞。start() 执行后会立即返回主进程继续向下执行不会等待子进程运行完毕1.2 进程等待与同步join(timeoutNone)机制阻塞当前调用方直至被调用的子进程执行完毕并退出或者达到了指定的 timeout 超时秒数意义用于实现主子进程的时间同步确保主进程获取到子进程运行后的数据或完成信号后再继续往下走1.3 进程状态查询is_alive()机制返回一个布尔值用于实时探测子进程当前的运行状态返回值True子进程已启动且尚未终止False子进程尚未调用 start()或已经正常/异常终止1.4 守护进程daemon 属性机制布尔值属性必须在调用 start() 之前设置行为特征False主进程代码执行完毕后会静默等待所有非守护子进程运行结束才会真正退出True子进程变为后台服务。一旦主进程的代码执行完毕所有守护子进程无论是否运行完都会被操作系统瞬间强制终止守护进程内部不能再创建新的子进程1.5 终止terminate() 与 kill()机制terminate()向子进程发送 SIGTERM 信号请求操作系统强制终止该进程。不会执行 finally 代码块kill()向子进程发送 SIGKILL 信号直接抹杀进程注意如果子进程在终止时正持有锁资源或在 Queue 中写入数据强制终止可能导致锁无法释放进而引发死锁或造成管道数据损坏。应谨慎使用1.6 退出码exitcode 属性机制用于获取进程终止时的退出状态代码数值含义None进程尚未终止仍在运行0进程正常执行完毕退出 0进程在运行中抛出未捕获的异常并退出代码通常为 1 0进程被信号强制终止。例如若被 terminate() 杀死exitcode 通常为 -15即 -SIGTERM2. 生命周期控制示例下面的代码展示了如何结合使用 join()、daemon、terminate() 和 exitcode 对多个子进程实施生命周期管理import time import os from multiprocessing import Process def normal_worker(duration: int): 常规工作进程 print(f[Worker] PID {os.getpid()} 开始工作...) time.sleep(duration) print(f[Worker] PID {os.getpid()} 正常工作结束。) def daemon_worker(): 后台监控进程守护进程 while True: print(f[Daemon] | PID: {os.getpid()}...) time.sleep(0.5) if __name__ __main__: print( 主进程启动 ) # 1. 创建普通子进程 p_worker Process(targetnormal_worker, args(2,), nameWorkerProcess) # 2. 创建守护子进程 p_daemon Process(targetdaemon_worker, nameDaemonProcess) p_daemon.daemon True # 必须在 start() 前设置 # 启动进程 p_worker.start() p_daemon.start() print(fp_worker 是否存活? {p_worker.is_alive()}) print(fp_daemon 是否存活? {p_daemon.is_alive()}) # 3. 演示 join() 超时控制与 terminate() # 假设我们给 p_worker 最多 1 秒的执行时间超时则强制终止 p_worker.join(timeout1.0) if p_worker.is_alive(): print(p_worker 执行超时强行终止) p_worker.terminate() p_worker.join() # 等待 OS 回收该进程刷新 exitcode # 4. 打印退出状态码 print(fp_worker exitcode: {p_worker.exitcode}) print(fp_worker 是否存活? {p_worker.is_alive()}) print(主进程退出)输出结果最佳实践组合使用 terminate() 和 join()调用 terminate() 后操作系统需要微小的响应时间来注销进程。紧接着调用 join() 可以确保主进程在继续向下推进前该子进程占用的系统句柄与 exitcode 状态已被彻底清理回收孤儿与僵尸进程在类 Unix 系统中若子进程退出而父进程未收集其状态子进程将短暂变为僵尸进程。善用 join() 是良好的资源回收习惯四、进程同步虽然操作系统为每个进程分配了独立的虚拟内存空间进程间无法直接篡改彼此的变量但在实际工程中多个进程常常需要共享某些外部物理资源——例如同一份磁盘文件、数据库连接等当多个进程同时试图读写同一个共享资源时就会触发竞态条件引发数据损坏、输出乱码或逻辑交错。为了保证资源的互斥访问我们需要引入进程锁1. 为什么需要锁假设我们有两个子进程同时向同一个文件写入数据或者同时向控制台打印字符。由于操作系统调度进程的随机性如果不对访问进行限制输出流可能会产生重叠和交叉交错未加锁乱序抢占: [进程A]: 正在写入[进程B]: 正在写入数据B...数据A... 加锁后排队互斥: [进程A]: 正在写入数据A... [进程B]: 正在写入数据B...关键概念临界区在代码中访问或修改共享资源的那段代码区域被称为临界区。进程锁的核心作用就是保证在任意时刻最多只有一个进程能够进入临界区执行代码2. LockPython 的 multiprocessing 模块提供了 Lock互斥锁类。它在底层依赖操作系统的信号量机制实现互斥锁拥有两种状态Unlocked任何进程都可以请求并获取该锁Locked锁已被某个进程持有其他试图获取该锁的进程将被阻塞挂起进入等待队列3. 获取锁与释放锁获取锁acquire(blockTrue, timeoutNone)参数说明blockTrue阻塞式获取。如果锁已被占用当前进程暂停执行直到锁被释放blockFalse非阻塞式获取。无论是否拿到锁方法会立刻返回。成功拿到锁返回 True锁被占用则返回 Falsetimeout设置最长等待时间秒。超过设定时间仍未拿到锁则返回 False释放锁release()机制将锁的状态重置为 Unlocked并唤醒等待队列中的下一个进程。约束只有持有该锁的进程才能调用 release()否则会抛出 ValueError4. 标准规范如果采用手动调用 acquire() 和 release() 的方式一旦临界区内部的代码在执行过程中抛出未捕获的异常就会导致 release() 被跳过这会让锁永远处于 Locked 状态引发严重的死锁导致后续所有等待该锁的进程被永久卡死# 危险做法缺少异常防护极易引发死锁 lock.acquire() try: unsafe_operation() # 若此处抛出异常release 永远不会执行 finally: lock.release()最佳实践与文件操作类似Lock 对象实现了上下文管理器协议。使用 with lock: 结构解释器会在进入代码块时自动调用 acquire()并在离开代码块时百分之百保证调用 release()# 标准做法安全、简洁、防死锁 with lock: # 临界区代码 unsafe_operation()5. 注意事项锁会降低并发性能锁的本质是将并行执行强制转化为串行执行。因此临界区内的代码应当尽可能精简只包含真正需要互斥的资源操作切忌将耗时的计算或网络请求放在锁内部Lock 对象必须通过参数传递在 Windows 和 macOS 系统下主进程创建的 Lock 对象必须通过 args 显示传递给子进程不能依赖全局变量否则子进程接收到的会是重新初始化过的独立锁导致锁机制失效嵌套锁引发死锁同一个进程试图对同一个非可重入锁连续调用两次 acquire()会导致该进程自己卡死自己。若需要递归加锁应使用 multiprocessing.RLock可重入锁五、进程通信在上一章中我们讨论了如何使用进程锁控制共享资源的访问。然而在大多数并发编程场景中进程之间不仅需要竞争资源更需要协同工作与传递数据由于操作系统为每个进程分配了独立的内存地址空间这导致了一个关键现象进程间无法直接共享普通变量1. 为什么不能共享变量在单进程或多线程程序中全局变量位于同一个内存空间内所有线程均可自由修改。但对于多进程而言子进程在创建时操作系统会为其分配独立的虚拟内存或者在写时拷贝机制下为其生成一份独立的内存副本全局变量在子进程中的独立性import os import time from multiprocessing import Process # 定义一个全局列表 global_data [] def modify_data(): global global_data global_data.append(Result) print(f子进程 [PID: {os.getpid()}] 内部的列表内容: {global_data}) if __name__ __main__: p Process(targetmodify_data) p.start() p.join() # 在主进程中检查全局变量 print(f主进程 [PID: {os.getpid()}] 内部的列表内容: {global_data})输出结果从结果可以看出子进程对 global_data 的修改只发生在它自己的内存副本中。子进程执行完毕退出后该内存空间被销毁主进程中的全局变量未受任何影响。为了在独立内存的进程间安全、高效地传输数据我们需要使用 Python 提供的进程间通信机制2. Queue在单进程中我们常使用 queue.Queue但在多进程环境下必须使用 multiprocessing.Queue底层原理multiprocessing.Queue 是线程和进程安全的。其底层依赖操作系统原生的管道与信号量/锁实现。当一个进程向队列 put() 数据时Python 会先将数据序列化为字节流通过管道传输给队列当另一个进程调用 get() 时再将字节流反序列化还原为 Python 对象常用 API方法签名作用描述阻塞控制put(obj, blockTrue, timeoutNone)写入数据若队列已满blockTrue 会阻塞等待blockFalse 或超时会抛出 queue.Full 异常get(blockTrue, timeoutNone)读取并弹出数据若队列为空blockTrue 会阻塞等待blockFalse 或超时会抛出 queue.Empty 异常empty()判空返回 boolfull()判满返回 boolqsize()获取元素数量返回 int3. Queue 使用案例在分布式系统中生产者-消费者模型是最基础解耦设计。通过向队列写入哨兵值可以优雅地通知消费者进程安全退出from multiprocessing import Process, Queue import time def producer(queue): 生产者进程向队列中放入数据 for item in [商品A, 商品B, 商品C]: queue.put(item) print(f生产者生产了{item}) time.sleep(1) # 发送结束标记 queue.put(None) def consumer(queue): 消费者进程从队列中取出数据 while True: item queue.get() if item is None: break print(f消费者取出了{item}) if __name__ __main__: queue Queue() producer_process Process( targetproducer, args(queue,) ) consumer_process Process( targetconsumer, args(queue,) ) producer_process.start() consumer_process.start() producer_process.join() consumer_process.join() print(生产和消费任务全部完成)输出结果4. Pipe除了 Queue 之外Python 还提供了更原生的点对点通信工具——Pipe管道Pipe 与 Queue 对比Queue允许多个生产者和多个消费者同时读写内部自带锁保护功能全面但由于有额外的缓冲区管理与同步开销性能略低于 PipePipe专门用于两个进程之间进行一对一的高性能通信。架构轻量吞吐量和传输速率显著高于 Queue函数签名与双工模式conn1, conn2 Pipe(duplexTrue)duplexTrue双向全双工返回两个端点连接对象 conn1 和 conn2。两端均可同时调用 send() 和 recv()duplexFalse单向半双工conn1 只能用于接收数据recv()conn2 只能用于发送数据send()端点连接对象常用 APIsend(obj)向管道写入一个可序列化的 Python 对象recv()从管道读取一个对象。若管道为空该方法会一直阻塞等待如果对端连接已被关闭会抛出 EOFErrorclose()关闭当前管道端点5. Pipe 使用案例下面的代码演示了 Pipe 建立双向通信渠道实现 主进程下发指令 - 子进程计算 - 返回结果from multiprocessing import Process, Pipe def worker(conn): # 接收主进程发送的数据 number conn.recv() # 计算结果并发送回主进程 conn.send(number ** 2) conn.close() if __name__ __main__: # 创建管道的两个端点 parent_conn, child_conn Pipe() # 将一个端点交给子进程 process Process( targetworker, args(child_conn,) ) process.start() # 主进程发送数据 parent_conn.send(12) # 主进程接收结果 result parent_conn.recv() print(f计算结果{result}) process.join() parent_conn.close()输出结果对比选择一对一高性能连接优先选择Pipe避免队列机制带来的额外性能开销。若涉及多生产者、多消费者或任务分发池优先选择Queue依靠其自带的锁与同步机制保障多端并发安全注意由于 Queue 和 Pipe 底层依赖 pickle 模块进行序列化切勿接收并处理来自不可信网络端的裸数据防止反序列化漏洞六、自定义 Process在前面的章节中我们主要通过向 Process(targetfunc) 传递函数指针的方式来创建子进程。这种函数式写法在编写简单脚本或临时任务时非常高效然而在面对复杂项目时将所有业务逻辑挤在一个独立函数里会导致代码耦合度高、难以复用。此时采用面向对象方式继承 Process 类是更优雅的选择1. 继承 Process 类通过自定义类继承 Process可以将进程相关的状态数据与执行逻辑封装在一个对象中。实现自定义进程类需要遵循以下原则继承基类显式继承 multiprocessing.Process重写 __init__在自定义类的 __init__ 方法中必须首先显式调用 super().__init__()以初始化基类重写 run() 方法将子进程实际要执行的业务代码写入 run() 方法中。切勿在外部手动调用 run()而应调用 start()2. start() 与 run()常见的一个错误是混淆 start() 与 run() 的触发时机调用 p.start()向操作系统申请创建新的子进程并在新进程的独立地址空间内异步触发 run() 方法。这才是真正的多进程并行调用 p.run()仅相当于在当前主进程中同步执行了一个普通类方法不会创建任何新的子进程程序依然是单线程顺序执行3. 使用案例下面的示例展示了如何实现一个自定义的 LogCleaner 类用于从共享队列中获取日志进行格式化清洗并将清洗后的合格数据传递至下一个管道from multiprocessing import Process, Queue class LogCleaner(Process): def __init__(self, task_queue, result_queue): super().__init__() self.task_queue task_queue self.result_queue result_queue def run(self): while True: log self.task_queue.get() # 收到结束标记退出子进程 if log is None: break cleaned_log log.strip().upper() self.result_queue.put(cleaned_log) if __name__ __main__: task_queue Queue() result_queue Queue() logs [ error: connection timeout , info: user logged in, warning: high memory usage ] # 创建两个工作进程 workers [ LogCleaner(task_queue, result_queue) for _ in range(2) ] for worker in workers: worker.start() # 添加任务 for log in logs: task_queue.put(log) # 每个子进程都需要一个结束标记 for _ in workers: task_queue.put(None) for worker in workers: worker.join() # 结果数量与原始任务数量相同 for _ in logs: print(result_queue.get())输出结果七、进程池在前面的章节中我们都是手动创建 Process 对象来执行多任务。如果需要处理大量短生命周期的任务为每个任务都手动实例化、启动并销毁会给操作系统带来极大的负担为高效管理大规模并发任务通常采用进程池机制1. 为什么需要进程池手动创建进程的痛点资源开销巨大频发地向操作系统申请创建与销毁进程包括 PCB 分配、物理内存映射、虚拟地址空间初始化吞吐效率低下宕机风险如果任务数量无节制增长同时创建 10000 个子进程会瞬间耗尽系统的 CPU 和 RAM 资源触发 OOM 导致操作系统崩溃进程池的核心思想资源复用与总量控制进程池在程序启动时预先创建好固定数量的子进程并维持其常驻内存。当有新任务提交时进程池从池中拣选空闲的 Worker 去执行任务执行完毕后子进程不会被销毁而是继续回到池中等待下一个任务2. Pool 类Python 标准库提供了 multiprocessing.Pool 类来快速构建进程池from multiprocessing import Pool Pool(processesNone, initializerNone, initargs(), maxtasksperchildNone) - processes指定进程池中允许同时运行的最大 Worker 进程数量 - initializer每个 Worker 进程启动时要优先执行的初始化函数 - maxtasksperchild指定每个 Worker 进程在处理多少个任务后自动退出并重启一个新的 Worker3. 进程池常见 APIPool 提供了多种将任务提交给进程池执行的方法核心可以归纳为同步与异步、单任务与批量任务两个维度apply()将任务提交给进程池并阻塞主进程直到该任务被分配的 Worker 执行完毕并返回结果pool.apply(func, args(), kwds{}) - Any缺点由于主进程会被完全阻塞apply()无法实现多任务并行只会在池中依次串行执行任务。因此极少使用apply_async()异步提交任务方法会立刻返回一个 AsyncResult 对象主进程不会被阻塞可以继续向池中下发后续任务pool.apply_async( func, args(), kwds{}, callbackNone, error_callbackNone ) - AsyncResult - callback指定任务成功完成后调用的回调函数接收 func 的返回值作为入参 - error_callback指定任务抛出未捕获异常时的回调函数接收异常对象作为入参AsyncResult 常用 APIget(timeoutNone)阻塞获取任务返回值wait(timeoutNone)阻塞等待任务执行完毕不获取返回值ready()返回 bool判断任务是否已执行完毕successful()返回 bool判断任务是否未触发异常成功结束map() 与 starmap()功能类似于 Python 内置的 map() 函数。它会将 iterable 中的每一个元素作为参数分发给 Worker 进程阻塞等待所有元素计算完毕并以列表形式返回全量结果pool.map(func, iterable, chunksizeNone) - list pool.starmap(func, iterable, chunksizeNone) - listmap() 与 starmap() 的区别map()适用于目标函数仅接收1 个入参的场景starmap()适用于目标函数接收多个入参的场景。它会自动将 iterable 中的元组元素进行解包传参4. 进程池生命周期对进程池进行显式销毁与资源回收时必须严格遵守调用顺序规范pool.close()关闭进程池的入口阻止后续继续提交新任务。如果不先调用 close()直接调用 join() 会抛出 ValueErrorpool.terminate()强行打断并终止池内的所有 Worker 正在执行的任务立即关闭进程池pool.join()阻塞等待进程池中已经提交的所有任务全部执行完毕5. 使用案例下面的代码展示了如何利用 Pool.starmap 进行批量矩阵数值计算以及利用 Pool.apply_async 实现带有异步回调的任务调度import time from multiprocessing import Pool def power(x, y): return x ** y def task(name): time.sleep(1) return f{name} 执行完成 def show_result(result): print(回调结果, result) if __name__ __main__: with Pool(4) as pool: # 批量执行多参数任务 data [ (2, 10), (3, 5), (4, 4) ] results pool.starmap(power, data) print(批量计算结果, results) # 异步提交任务 for name in [任务A, 任务B, 任务C]: pool.apply_async( task, args(name,), callbackshow_result ) # 等待异步任务完成 pool.close() pool.join() print(所有任务执行完成)输出结果总结本章围绕 Python 多进程编程展开首先介绍了并发、并行、同步和异步等基本概念帮助我们建立了并发编程的整体认识。随后学习了 Process 类的使用方法、进程的创建与控制、进程锁以及 join()、terminate()、守护进程等常用机制并理解了进程之间相互独立、不能直接共享变量的特点。最后我们学习了 Queue 和 Pipe 两种进程通信方式、继承 Process 类创建进程的方法以及进程池的使用使我们能够更加高效地管理和利用多个进程完成复杂任务。下一篇文章我们将学习Python 多线程编程深入了解线程的创建与管理、线程池、全局解释器锁以及多线程与多进程的适用场景进一步掌握 Python 并发编程的核心知识