Python并发编程实战:进程、线程与协程的核心区别与应用场景

📅 2026/8/17 23:15:59
Python并发编程实战:进程、线程与协程的核心区别与应用场景
1. 从一次线上故障说起为什么你需要分清进程、线程和协程去年我负责的一个数据同步服务出了个不大不小的线上问题。服务逻辑很简单从消息队列里消费数据做一些清洗转换然后批量写入数据库。为了提升吞吐量我们很自然地用上了多线程一个线程处理一条消息。上线初期一切正常但随着业务量翻了几番服务开始频繁出现响应延迟甚至偶尔会“卡死”几秒钟监控面板上的线程数曲线像坐过山车一样。我们一开始怀疑是数据库连接池不够用扩容后问题依旧。直到深入排查才发现根源在于我们对Python的线程模型理解有偏差——在CPU密集型的转换逻辑中大量线程的频繁切换和GIL全局解释器锁的争用导致了严重的性能退化。那次踩坑让我彻底明白在Python的世界里并发编程不是简单地“开个线程”就完事了你必须清楚地知道手里的“武器”——进程、线程、协程——各自的能力边界和适用场景否则就是在给系统埋雷。今天我们就来彻底理清这三者的区别。这不是一篇罗列概念的教科书而是一个踩过坑的工程师结合大量实战代码为你梳理出的“生存指南”。我们会从操作系统最底层的调度单元“进程”开始讲到更轻量的“线程”最后深入到Python中高效的“协程”不仅告诉你它们是什么更会重点剖析在什么情况下该用谁以及用的时候有哪些“坑”等着你。无论你是刚接触并发编程的新手还是想深化理解的进阶开发者这篇文章都能帮你建立起清晰、实用的知识框架。2. 进程拥有独立王国的“重型战舰”当你双击一个.exe文件或者在命令行输入python script.py并回车时操作系统就会为你创建一个进程。你可以把进程想象成一个拥有独立领地的王国。这个王国里有自己专属的内存空间堆、栈、数据段、一套完整的资源打开的文件、网络连接以及至少一位“国民”——也就是执行线程。不同的进程之间领地是严格隔离的一个进程崩溃了通常不会直接影响另一个进程因为它们的内存空间不互通。这种隔离性带来了极高的稳定性但代价是“建国”和“王国间通信”的成本非常高昂。2.1 进程的核心特征与创建在Python中我们使用multiprocessing模块来创建和管理进程。这是最接近操作系统原语的方式。import multiprocessing import os import time def heavy_computation_task(name): 模拟一个耗时的计算任务 print(f进程 {name} (PID: {os.getpid()}) 开始运行父进程ID: {os.getppid()}) result 0 for i in range(10000000): # 一个CPU密集型计算 result i * i print(f进程 {name} 计算完成结果: {result}) return result if __name__ __main__: # 在Windows系统上使用多进程必须有的保护 print(f主进程 PID: {os.getpid()}) # 创建进程对象 process1 multiprocessing.Process(targetheavy_computation_task, args(Alpha,)) process2 multiprocessing.Process(targetheavy_computation_task, args(Beta,)) start_time time.time() # 启动进程 process1.start() process2.start() # 等待进程结束 process1.join() process2.join() end_time time.time() print(f多进程总执行时间: {end_time - start_time:.2f} 秒)运行这段代码你会看到两个子进程输出了与主进程不同的PID进程ID并且它们几乎同时开始、同时结束计算。这就是多进程的威力真正的并行计算。因为每个进程都有独立的Python解释器和内存空间所以它们可以同时利用多个CPU核心完美绕开GIL的限制。注意上面代码中的if __name__ __main__:在Windows系统下是必须的。因为Windows没有Unix系的fork系统调用它通过重新导入模块的方式来创建子进程。如果没有这行保护子进程在导入模块时会再次执行模块顶层的代码可能导致无限递归创建进程。在Linux/macOS上虽然不强制但加上它也是良好的编程习惯。2.2 进程间通信IPC王国间的外交使节进程间内存隔离那它们怎么交换数据呢这就需要“进程间通信”。multiprocessing模块提供了几种方式队列Queue最常用基于管道和锁实现是线程安全的。管道Pipe更底层的双向或单向通信通道。共享内存Value, Array在内存中开辟一块双方都能访问的区域效率最高但需要自己处理同步问题。管理器Manager可以创建共享的列表、字典等复杂数据结构但速度较慢。下面是一个使用Queue进行生产者-消费者模型的例子import multiprocessing import time import random def producer(queue, items): 生产者进程生成数据并放入队列 for item in items: print(f生产者 放入: {item}) queue.put(item) time.sleep(random.uniform(0.1, 0.5)) # 模拟生产耗时 # 放入结束信号 queue.put(None) def consumer(queue, name): 消费者进程从队列取出并处理数据 while True: item queue.get() if item is None: # 收到结束信号 queue.put(None) # 为其他消费者传递信号如果有多个 print(f消费者 {name} 结束。) break print(f消费者 {name} 处理: {item}) time.sleep(random.uniform(0.2, 0.8)) # 模拟处理耗时 if __name__ __main__: # 创建一个进程间通信的队列 task_queue multiprocessing.Queue(maxsize5) # 设置队列最大容量 # 创建进程 prod multiprocessing.Process(targetproducer, args(task_queue, [A, B, C, D, E])) cons1 multiprocessing.Process(targetconsumer, args(task_queue, C1)) cons2 multiprocessing.Process(targetconsumer, args(task_queue, C2)) prod.start() cons1.start() cons2.start() prod.join() cons1.join() cons2.join() print(所有任务完成。)为什么用multiprocessing.Queue而不是queue.Queue后者是线程队列只能在同一个进程内的多个线程间使用。进程队列在底层做了序列化pickle和反序列化的处理使得对象可以在不同内存空间之间传递这是有性能开销的。所以进程间通信的数据量不宜过大且最好是可序列化的对象。2.3 进程池管理你的“舰队”手动管理大量进程很麻烦容易造成资源泄露。multiprocessing.Pool提供了一个进程池可以方便地提交任务由池子自动分配进程执行。import multiprocessing import time def compute_square(number): 计算一个数的平方模拟耗时任务 time.sleep(0.5) # 模拟I/O或计算延迟 return number * number if __name__ __main__: numbers list(range(1, 11)) # 创建一个包含4个工作进程的池 with multiprocessing.Pool(processes4) as pool: # 方法一: apply_async (异步不阻塞主进程) print(--- 使用 apply_async (异步提交) ---) results_async [] for num in numbers: # 提交任务到池立即返回一个AsyncResult对象 result_obj pool.apply_async(compute_square, (num,)) results_async.append(result_obj) # 稍后获取所有结果 squares_async [res.get() for res in results_async] # .get()会阻塞直到结果就绪 print(f异步结果: {squares_async}) # 方法二: map (同步但内部并行) print(\n--- 使用 map ---) squares_map pool.map(compute_square, numbers) print(fmap结果: {squares_map}) # 方法三: imap_unordered (迭代器结果顺序不保证哪个先完成先返回哪个) print(\n--- 使用 imap_unordered (按完成顺序) ---) for result in pool.imap_unordered(compute_square, numbers): print(f收到结果: {result}, end | )进程池使用心得mapvsapply_asyncmap更简洁但它会阻塞直到所有任务完成且一次性将可迭代对象转换成列表如果任务列表巨大可能消耗大量内存。apply_async更灵活可以非阻塞地提交并配合回调函数适合流式处理或需要精细控制的任务。进程数设置通常设置为CPU核心数或者CPU核心数 1。对于I/O密集型任务可以适当调高但进程创建本身有开销不是越多越好。可以用multiprocessing.cpu_count()获取逻辑核心数。池的上下文管理器使用with语句可以确保池在使用后被正确关闭和回收资源避免僵尸进程。进程的优缺点总结优点真正并行能利用多核CPU内存隔离稳定性高一个进程崩溃不影响其他。缺点创建和销毁开销大内存占用高每个进程都有独立的Python解释器和内存空间进程间通信复杂且速度慢。适用场景CPU密集型计算如图像处理、科学计算、复杂算法以及需要高稳定性、隔离性的任务。3. 线程共享王国的“轻骑兵”如果说进程是一个独立王国那么线程就是在这个王国内部并行工作的多个“轻骑兵”。它们共享进程的所有资源同样的内存空间、打开的文件描述符、全局变量。创建和切换线程的代价比进程小得多。但在Python中有一个著名的“阿喀琉斯之踵”——全局解释器锁。3.1 GILPython线程的“紧箍咒”GIL是一把锁它规定在同一个时刻只有一个线程可以执行Python字节码。这意味着即使在多核CPU上一个Python进程内的多个线程也无法实现真正的并行计算它们依然是“并发”执行快速交替而非“并行”。为什么要有GIL主要是为了简化CPython解释器的内存管理。Python使用引用计数来管理内存如果没有GIL多个线程同时修改一个对象的引用计数会导致计数错误进而引发内存泄漏或错误释放。GIL用一把大锁避免了复杂的细粒度锁管理牺牲了多核并行能力换来了实现的简单和单线程下的效率。所以一个重要的结论是在Python中多线程对于CPU密集型任务基本没有性能提升甚至可能因为线程切换和锁竞争而变慢。它的主要用武之地是I/O密集型任务。3.2 线程的创建与同步Python通过threading模块来操作线程。import threading import time import random def io_bound_task(task_id): 模拟一个I/O密集型任务比如网络请求或磁盘读写 print(f线程 {task_id}: 开始I/O操作...) time.sleep(random.uniform(1, 3)) # 模拟I/O等待时间 print(f线程 {task_id}: I/O操作完成。) return task_id if __name__ __main__: threads [] start_time time.time() # 创建并启动10个线程 for i in range(5): t threading.Thread(targetio_bound_task, args(i,)) threads.append(t) t.start() # 启动线程非阻塞 # 等待所有线程完成 for t in threads: t.join() end_time time.time() print(f多线程总执行时间: {end_time - start_time:.2f} 秒) # 你会看到总时间远小于 5 * (1~3秒)因为线程在等待I/O时会让出GIL其他线程可以运行。运行这个例子你会发现5个模拟I/O任务的总耗时接近其中最慢的那个任务的时间而不是它们的累加。这是因为当一个线程执行到time.sleep()模拟I/O阻塞时它会释放GIL其他线程就能获得GIL并执行。这样在等待I/O的时间里CPU可以去做其他事情从而大幅提升整体效率。3.3 线程安全与锁机制多个线程共享内存这就带来了“线程安全”问题。如果多个线程同时读写同一个变量可能会产生不可预料的结果。import threading # 一个不安全的计数器 class UnsafeCounter: def __init__(self): self.value 0 def increment(self): # 这三步操作不是原子的读取 - 修改 - 写回 temp self.value temp temp 1 self.value temp # 一个安全的计数器使用锁 class SafeCounter: def __init__(self): self.value 0 self._lock threading.Lock() # 创建一把锁 def increment(self): with self._lock: # 使用with语句自动获取和释放锁 temp self.value temp temp 1 self.value temp def test_counter(counter_class, num_threads100, increments_per_thread1000): counter counter_class() threads [] def worker(): for _ in range(increments_per_thread): counter.increment() for _ in range(num_threads): t threading.Thread(targetworker) threads.append(t) t.start() for t in threads: t.join() expected num_threads * increments_per_thread actual counter.value print(f{counter_class.__name__}: 期望值 {expected}, 实际值 {actual}, 是否正确? {expected actual}) if __name__ __main__: print(测试不安全计数器:) test_counter(UnsafeCounter, num_threads10, increments_per_thread1000) print(\n测试安全计数器:) test_counter(SafeCounter, num_threads10, increments_per_thread1000)多次运行UnsafeCounter的结果很可能不是10000而SafeCounter则总是正确的。threading.Lock确保了同一时间只有一个线程能执行with lock:块内的代码。锁的使用注意事项粒度要细锁住的范围越小越好只保护共享数据的关键部分。锁住大段代码会严重降低并发性能。避免死锁当两个或多个线程互相等待对方释放锁时就会发生死锁。常见的解决方案是按固定顺序获取锁、使用超时机制lock.acquire(timeout5)、或者使用更高级的同步原语如threading.RLock可重入锁同一个线程可以多次获取或threading.Condition。优先使用with语句with lock:能确保锁在任何情况下包括发生异常时都会被释放比手动acquire()和release()更安全。3.4 线程局部数据有时候你希望某些数据只对某个线程可见而不是全局共享。threading.local()可以做到这一点。import threading import time # 创建一个线程局部存储对象 local_data threading.local() def show_data(): 显示当前线程的局部数据 try: value local_data.value except AttributeError: print(f线程 {threading.current_thread().name}: 还没有设置值) return print(f线程 {threading.current_thread().name}: value {value}) def worker(num): 每个线程设置自己的局部数据 local_data.value num # 这个赋值只影响当前线程的local_data实例 time.sleep(0.1) show_data() if __name__ __main__: threads [] for i in range(3): t threading.Thread(targetworker, args(i,)) threads.append(t) t.start() # 主线程也尝试访问 show_data() # 主线程没有设置过local_data.value会抛出AttributeError for t in threads: t.join()threading.local()为每个线程创建了一个独立的命名空间常用于存储数据库连接、用户会话等需要隔离的数据。线程的优缺点总结优点创建和切换开销小共享内存数据交换方便快捷。缺点受GIL限制无法用于加速CPU密集型任务需要程序员处理复杂的线程同步问题容易引入bug竞态条件、死锁。适用场景I/O密集型任务如网络爬虫、Web服务器处理请求、文件批量处理其中等待外部响应的空闲时间可以被其他线程利用。4. 协程用户态调度的“超级纤维”协程可以理解为“用户态的轻量级线程”。它的核心特点是由程序员在代码中显式控制切换而不是由操作系统内核调度。当一个协程遇到I/O操作时它会主动让出执行权而不是被操作系统强制挂起。这样切换发生在用户态开销极小仅仅是寄存器上下文和栈的保存/恢复不涉及内核态切换。在Python中协程主要通过asyncio库和async/await语法来实现。它特别适合处理大量高并发的I/O密集型任务比如微服务、实时消息推送、爬虫等。4.1 从生成器到异步IO理解协程可以从生成器开始。生成器函数使用yield可以在执行过程中暂停并向调用者返回一个值后续可以从暂停点恢复执行。协程在此基础上更进一步不仅可以产出值还可以消费值通过.send(value)并且与事件循环结合实现了高效的并发。asyncio是Python 3.4引入的标准库它提供了一个事件循环Event Loop负责调度和执行协程。async用于声明一个函数是异步函数await用于在异步函数中等待另一个异步操作完成。import asyncio import time async def say_after(delay, what): 一个简单的异步函数模拟I/O等待 await asyncio.sleep(delay) # 异步等待让出控制权 print(what) return f{what} done after {delay}s async def main_sequential(): 顺序执行两个异步任务 print(f顺序执行开始 at {time.strftime(%X)}) result1 await say_after(2, Hello) # 等待2秒 result2 await say_after(1, World) # 再等待1秒 print(f顺序执行结束 at {time.strftime(%X)}) print(f结果: {result1}, {result2}) async def main_concurrent(): 并发执行两个异步任务 print(f并发执行开始 at {time.strftime(%X)}) # 创建两个协程任务对象 task1 asyncio.create_task(say_after(2, Hello)) task2 asyncio.create_task(say_after(1, World)) # 等待两个任务都完成 result1 await task1 result2 await task2 print(f并发执行结束 at {time.strftime(%X)}) print(f结果: {result1}, {result2}) if __name__ __main__: print(--- 顺序执行 ---) asyncio.run(main_sequential()) # asyncio.run() 负责创建事件循环并运行主协程 print(\n--- 并发执行 ---) asyncio.run(main_concurrent())运行结果会清晰地展示差异顺序执行总耗时约3秒而并发执行总耗时约2秒等于最长的那个任务时间。在main_concurrent中task1和task2被同时创建并加入事件循环。当task1执行到await asyncio.sleep(2)时它挂起并让出控制权事件循环立即去执行task2。task2的sleep(1)先完成打印“World”然后事件循环在适当的时候恢复task1。整个过程只有一个线程但通过协程的协作式调度实现了高效的并发。4.2 核心概念任务、Future与事件循环协程Coroutine用async def定义的函数调用它返回一个协程对象但不会立即执行。任务Task是对协程的进一步封装它调度协程在事件循环中执行。asyncio.create_task()将一个协程包装成任务并立即排入事件循环。Future一个更低层级的对象代表一个异步操作的最终结果。Task是Future的子类。我们通常直接操作Task就够了。事件循环Event Loop异步编程的核心。它管理着所有的协程和任务负责在它们之间调度并在有I/O事件完成时唤醒对应的协程。4.3 实战用协程编写一个简单的Web请求并发器让我们看一个更贴近实际的例子并发获取多个网页的标题。import asyncio import aiohttp # 需要安装: pip install aiohttp from bs4 import BeautifulSoup # 需要安装: pip install beautifulsoup4 async def fetch_title(session, url): 异步获取一个URL的页面标题 try: # 发起异步HTTP GET请求 async with session.get(url, timeout10) as response: html await response.text() soup BeautifulSoup(html, html.parser) title soup.title.string.strip() if soup.title else No Title return url, title except Exception as e: return url, fError: {e} async def main(): urls [ https://www.python.org, https://www.github.com, https://www.example.com, https://httpbin.org/delay/2, # 一个会延迟2秒响应的测试URL ] # 创建一个aiohttp客户端会话复用TCP连接提升性能 async with aiohttp.ClientSession() as session: # 为每个URL创建一个获取任务 tasks [asyncio.create_task(fetch_title(session, url)) for url in urls] # 等待所有任务完成并收集结果 results await asyncio.gather(*tasks, return_exceptionsFalse) for url, title in results: print(f{url[:30]:30} - {title[:50]}) if __name__ __main__: # 在Windows上如果遇到事件循环问题可以设置以下策略高版本Python通常不需要 # asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy()) asyncio.run(main())代码解析与避坑指南使用aiohttp代替requests标准的requests库是同步的在协程中使用await requests.get()会阻塞整个事件循环。aiohttp是专为asyncio设计的异步HTTP客户端/服务器库。复用ClientSession为每个请求都创建新的会话开销很大。最佳实践是在一个async with块内创建一个会话并在所有请求中复用。它会自动管理连接池。asyncio.gather()这是一个非常有用的函数用于并发运行多个可等待对象协程、任务、Future并等待它们全部完成。return_exceptionsTrue参数可以让它在某个任务出错时继续收集其他结果而不是立即抛出异常。异常处理异步代码中的异常传播路径和同步代码不同。务必在协程内部用try...except捕获和处理异常或者在gather等地方统一处理。阻塞操作是“毒药”绝对不要在协程内使用任何同步的、会阻塞线程的操作比如time.sleep()、同步的文件读写、requests.get()等。这会让整个事件循环停下来。必须使用它们的异步替代品asyncio.sleep()、aiofiles、aiohttp等。4.4 协程与线程池的协作有时候你不得不调用一些遗留的、不支持异步的同步函数比如某些数据库驱动、计算库。在协程里直接调用它会阻塞事件循环。这时可以将其放到一个线程池中执行避免阻塞主线程。import asyncio import time from concurrent.futures import ThreadPoolExecutor def blocking_io_task(n): 一个模拟的同步阻塞I/O任务 print(f阻塞任务 {n} 在线程 {threading.current_thread().name} 中开始) time.sleep(2) # 模拟阻塞 print(f阻塞任务 {n} 完成) return n * 10 async def main(): print(f主协程开始 at {time.strftime(%X)}) # 创建一个线程池执行器 loop asyncio.get_running_loop() # 提交阻塞任务到线程池返回一个asyncio.Future对象 # run_in_executor 默认使用 ThreadPoolExecutor future1 loop.run_in_executor(None, blocking_io_task, 1) future2 loop.run_in_executor(None, blocking_io_task, 2) # 异步地等待线程池中的任务完成 result1 await future1 result2 await future2 print(f结果: {result1}, {result2}) print(f主协程结束 at {time.strftime(%X)}) if __name__ __main__: asyncio.run(main())loop.run_in_executor()将同步函数“伪装”成一个异步任务让事件循环可以在等待线程池结果的同时去处理其他协程。这是一种“曲线救国”的方式但引入了线程开销和潜在的GIL问题应谨慎使用。协程的优缺点总结优点极高的并发能力单线程可轻松处理成千上万个连接上下文切换开销极小远低于线程没有锁的问题在单线程内。缺点编程模型与传统的同步代码差异较大有学习成本所有相关库都必须支持异步生态仍在完善中调试相对复杂一个协程中的阻塞调用会拖垮整个事件循环。适用场景高并发的I/O密集型应用如Web服务器FastAPI, aiohttp、实时消息系统、爬虫、微服务网关等。5. 终极对决如何根据场景选择现在我们对三驾马车都有了深入理解。在实际项目中如何选择下面这个决策流程图和对比表可以帮你快速判断决策流程你的任务是CPU密集型吗如图像处理、视频编码、复杂数学计算是- 选择多进程(multiprocessing)。利用多核优势。否- 进入第2步。你的任务是I/O密集型吗如网络请求、数据库查询、磁盘读写且并发量非常高数千以上是- 优先考虑协程(asyncio)。性能最佳资源占用最少。否- 进入第3步。你的任务是I/O密集型但并发量一般几十到几百或者需要调用大量不支持异步的第三方库是- 选择多线程(threading) 或线程池(concurrent.futures.ThreadPoolExecutor)。编程模型简单生态兼容性好。是否需要极高的稳定性和隔离性如一个任务崩溃绝不能影响其他任务是- 选择多进程。内存隔离是最大的安全保障。特性对比表特性进程 (Process)线程 (Thread)协程 (Coroutine)数据隔离完全隔离不共享内存共享进程内存共享进程内存创建/切换开销大系统调用独立内存中等系统调用但共享内存极小用户态切换并行能力是可利用多核否受GIL限制并发执行否单线程内并发执行编程复杂度中需处理IPC高需处理线程同步、锁中高异步编程思维调试难度中高竞态条件难复现中高执行流非直观适用场景CPU密集型计算、需要隔离的独立任务I/O密集型、GUI应用、调用阻塞库超高并发I/O、微服务、实时通信代表模块multiprocessing,concurrent.futures.ProcessPoolExecutorthreading,concurrent.futures.ThreadPoolExecutorasyncio,anyio,trio个人经验与避坑指南不要过早优化如果你的应用并发量不大比如简单的脚本或后台任务用简单的同步代码或线程池就够了。异步编程的复杂性可能得不偿失。理解GIL的真相GIL只影响CPU密集型的Python字节码执行。对于I/O操作、C扩展中释放了GIL的计算如NumPy, pandas的部分操作多线程依然可以有效利用多核。不要一棍子打死多线程。进程池的序列化坑multiprocessing.Pool在向工作进程传递参数和返回结果时会使用pickle进行序列化。确保你传递的对象是可序列化的。自定义的类可能需要实现__getstate__和__setstate__方法。异步代码的“遗忘await”这是最常见的异步bug。忘记写await会导致协程对象没有被实际执行错误静默发生。使用像asyncio.run()这样的高层API并配合类型检查工具如mypy可以帮助发现这类问题。资源清理无论是进程、线程还是协程都要确保资源被正确关闭。对于进程/线程使用join()等待其结束对于协程任务确保它们被await或cancel对于网络连接、文件句柄使用async with上下文管理器。监控与观测并发程序的行为更难预测。一定要加入完善的日志和监控。记录线程/进程ID、任务开始结束时间、关键状态。使用像threading.enumerate()、multiprocessing.active_children()来查看存活的工作单元。6. 混合使用案例构建一个简单的并行计算服务最后我们来看一个综合案例一个Web服务接收一个数字列表后端同时使用多进程进行CPU密集型计算比如判断质数并使用协程处理并发的HTTP请求。这个例子会用到FastAPI异步Web框架和multiprocessing。# server.py import asyncio from concurrent.futures import ProcessPoolExecutor from fastapi import FastAPI, BackgroundTasks import multiprocessing import time from typing import List app FastAPI() def is_prime_cpu_intensive(n: int) - bool: 一个模拟的CPU密集型计算判断质数简单版本 if n 2: return False if n 2: return True if n % 2 0: return False # 为了模拟计算压力我们故意用低效算法并增加循环 sqrt_n int(n**0.5) 1 for i in range(3, sqrt_n, 2): # 模拟更多计算 _ [j for j in range(1000)] if n % i 0: return False return True def process_numbers(numbers: List[int]) - List[bool]: 在进程池中并行处理数字列表 with ProcessPoolExecutor(max_workersmultiprocessing.cpu_count()) as executor: results list(executor.map(is_prime_cpu_intensive, numbers)) return results # 创建一个全局的进程池执行器避免每次请求都创建销毁 process_executor ProcessPoolExecutor(max_workersmultiprocessing.cpu_count()) app.post(/check-primes) async def check_primes(numbers: List[int], background_tasks: BackgroundTasks): 接收一个数字列表返回每个数字是否为质数。 使用后台任务在进程池中进行CPU密集型计算避免阻塞事件循环。 start_time time.time() # 将CPU密集型任务提交到进程池 loop asyncio.get_event_loop() # 注意这里将同步函数 process_numbers 放到了进程池中执行 # 更优雅的做法是将 is_prime_cpu_intensive 直接映射到进程池 # 这里为了演示结构做了一层包装 prime_flags await loop.run_in_executor( process_executor, lambda: [is_prime_cpu_intensive(n) for n in numbers] ) # 构建结果 result [ {number: num, is_prime: is_prime, processed_by: fprocess_{multiprocessing.current_process().pid}} for num, is_prime in zip(numbers, prime_flags) ] elapsed time.time() - start_time return { results: result, total_numbers: len(numbers), time_elapsed: f{elapsed:.3f}秒, note: CPU密集型计算在独立的进程池中完成未阻塞主事件循环。 } app.on_event(shutdown) def shutdown_event(): 应用关闭时清理进程池 process_executor.shutdown(waitTrue) print(进程池已关闭。) if __name__ __main__: import uvicorn # 启动服务器 uvicorn.run(app, host0.0.0.0, port8000)这个架构的精髓FastAPI处理HTTP请求它是异步框架使用asyncio可以高效处理大量并发连接I/O密集型。进程池处理CPU任务质数判断是纯CPU计算放在主线程也是唯一的事件循环线程会彻底阻塞所有请求。因此我们使用ProcessPoolExecutor创建一个进程池将计算任务“卸载”到独立的进程中去。run_in_executor桥接异步与同步在异步的请求处理函数中我们通过loop.run_in_executor将同步的CPU计算函数提交到进程池。这个方法返回一个asyncio.Future我们可以用await等待它而不会阻塞事件循环去处理其他 incoming 的HTTP请求。资源管理我们创建了一个全局的ProcessPoolExecutor并在应用关闭时清理它。避免为每个请求创建/销毁进程池的巨大开销。你可以用以下命令测试需要先安装fastapi,uvicornpip install fastapi uvicorn python server.py然后用curl或Postman发送POST请求到http://localhost:8000/check-primesBody为JSON{numbers: [101, 102, 103, 104, 105, 106, 107, 108, 109, 110]}。这个例子展示了如何在一个应用中让协程负责高并发的I/OHTTP让进程负责并行的CPU计算各司其职发挥最大效能。而线程在这个场景下由于GIL的存在无法加速CPU计算所以没有采用。希望这篇超过万字的总结能帮你彻底理清Python中进程、线程和协程的脉络。记住没有银弹只有最适合场景的工具。理解它们的本质才能在面对并发挑战时做出最明智的选择。