Python 并发八股线程、进程、协程和 asyncio 到底怎么选开场Python 并发不是一句“有 GIL所以多线程没用”前两篇我们先把 Python 的对象、引用、装饰器、生成器和上下文管理器讲完了。今天继续补一个后端面试绕不开的问题Python 有 GIL多线程是不是没用IO 密集任务到底用线程还是 asyncioCPU 密集任务为什么推荐多进程协程和线程到底差在哪里async def写了以后代码就一定不会阻塞吗FastAPI 里为什么有时建议写def有时又建议写async def很多回答会停在一句话Python 有 GIL所以多线程不能利用多核。这句话不算错但太粗了。真正能过面试、能落项目的回答至少要补上三层判断你的任务瓶颈是 CPU 计算还是网络、文件、数据库等待你想要的是并发处理更多等待任务还是并行压满多个 CPU 核你用的库是同步阻塞库还是支持await的异步库所以今天这篇不按 API 清单讲而是按选择题来讲Python 并发模型的核心不是“线程、进程、协程谁更高级”而是先判断任务瓶颈再选择执行单元和调度方式。一、先分清并发和并行先把两个词拆开。并发强调“同时处理多个任务的能力”。它不要求同一时刻真的有多个 CPU 核在执行代码。一个线程也可以通过事件循环在多个等待任务之间切换。并行强调“同一时刻真的有多个执行单元在运行”。比如多个 CPU 核同时跑计算任务。用后端请求举例并发 一个服务同时处理 1000 个请求 其中大部分时间在等网络、数据库、Redis 并行 多个 CPU 核同时计算图片压缩、特征提取、加密、模型推理这就是为什么 Web 后端经常更关心并发而数据计算更关心并行。如果任务大部分时间在等 IO你需要的是“等待期间别闲着”。线程和 asyncio 都可能有价值。如果任务大部分时间都在吃 CPU你需要的是“多个核一起算”。默认 CPython 里多进程通常比多线程更直接。二、GIL它限制的是同一进程内多个线程执行 Python 字节码GIL 全称是 Global Interpreter Lock全局解释器锁。Python 官方 C API 文档对它的描述很直接在常规 CPython 构建里线程访问 Python 对象前必须持有 GIL同一时刻通常只有持有 GIL 的线程能操作 Python 对象或调用 Python C API。翻译成面试语言在默认 CPython 里同一个进程内多个 Python 线程不能同时并行执行 Python 字节码。这句话有三个限定词很重要。第一是默认 CPython。Python 是语言CPython 是最常见的解释器实现。我们日常说的 GIL通常是在说 CPython 的实现细节。第二是同一个进程内的多个线程。多进程不是共享同一个解释器状态每个进程有自己的 Python 解释器和 GIL所以可以利用多个 CPU 核。第三是执行 Python 字节码。阻塞 IO 时GIL 可以释放。官方文档也明确提到文件读写这类阻塞 IO 周围会释放 GIL。所以 IO 密集任务里多线程仍然可能有价值。这也是“Python 多线程没用”这句话的问题它把 CPU 密集和 IO 密集混在一起了。GIL 为什么存在最常见的解释是CPython 的对象模型和引用计数需要线程安全。假设两个线程同时操作同一个对象的引用计数如果没有同步保护最简单的加一减一都可能出错。GIL 用一把大锁保护解释器内部状态实现成本更低也让大量 C 扩展和内置对象的行为更容易维护。代价就是CPU 密集型纯 Python 多线程不能线性利用多核。Python 3.13 以后的 free-threading 怎么看从 Python 3.13 开始CPython 支持一种可选的 free-threaded 构建可以禁用 GIL。PEP 703 也已经被接受。但面试和大多数生产项目里回答不能直接说“以后没有 GIL 了”。更稳的说法是CPython 已经在推进可选 free-threaded 构建但常规环境和大量三方包仍然要按默认 GIL 模型理解。写并发代码时依然要先区分 IO 密集、CPU 密集和库是否线程安全。这能体现你知道新变化也不会把实验性或可选能力说成所有生产环境的默认现实。三、线程适合 IO 等待不适合用来硬扛 CPU 计算Python 的threading模块提供线程级并发。线程在同一个进程里运行内存空间共享所以传递对象很方便。线程适合这些场景同时发多个 HTTP 请求。同时读写多个文件。后台跑一些轻量阻塞任务。调用没有 async 版本的阻塞库。在 FastAPI / Starlette 里把同步函数放到线程池避免堵住事件循环。线程不适合这些场景纯 Python 大循环计算。图片、视频、压缩、加密这类 CPU 密集任务。希望多个线程同时压满多个 CPU 核的计算任务。一个典型线程池写法fromconcurrent.futuresimportThreadPoolExecutor,as_completedimportrequestsdeffetch(url:str)-tuple[str,int]:resprequests.get(url,timeout3)returnurl,resp.status_code urls[https://docs.python.org/3/library/threading.html,https://docs.python.org/3/library/asyncio.html,https://docs.python.org/3/library/multiprocessing.html,]withThreadPoolExecutor(max_workers8)aspool:futures[pool.submit(fetch,url)forurlinurls]forfutureinas_completed(futures):print(future.result())这个例子里线程的价值不是让 Python 代码并行计算而是一个请求在等网络响应时其他线程可以继续发请求或处理返回结果。线程最大的坑共享状态线程共享进程内存这既是优点也是坑。看一个危险写法counter0defincr():globalcounterfor_inrange(10000):counter1counter 1看起来是一句底层并不是不可分割的原子操作。多个线程同时改共享变量需要锁、队列或者尽量避免共享可变状态。更稳的方式是fromqueueimportQueuefromthreadingimportThread queueQueue()defworker():whileTrue:itemqueue.get()try:handle(item)finally:queue.task_done()面试里如果只说“线程适合 IO 密集”还不够。最好补一句线程共享内存通信方便但共享可变状态需要同步控制常见做法是用 Lock、Queue 或减少共享状态。四、进程适合 CPU 密集但要付出通信和启动成本multiprocessing走的是进程级并发。官方文档也说得很清楚它通过子进程绕开 GIL让程序可以利用多个处理器。所以 CPU 密集任务优先考虑多进程批量图片处理。文本特征提取。CPU 计算型数据清洗。压缩、加密、解析大文件。不依赖 GPU 的本地推理或预处理。典型写法fromconcurrent.futuresimportProcessPoolExecutordefis_prime(n:int)-bool:ifn2:returnFalseforiinrange(2,int(n**0.5)1):ifn%i0:returnFalsereturnTruenumbers[112272535095293,112582705942171,115280095190773]withProcessPoolExecutor()aspool:forresultinpool.map(is_prime,numbers):print(result)多进程为什么能绕开 GIL因为每个进程都有自己的解释器、自己的内存空间、自己的 GIL。多个进程可以被操作系统调度到不同 CPU 核上真正并行运行。多进程的代价多进程不是“比线程高级”它只是解决了另一类问题。它的成本很明确进程启动比线程重。进程之间不共享普通内存。参数和返回值通常需要序列化。函数、参数、返回结果要能被 pickle。Windows 和 macOS 默认使用spawn子进程需要重新导入模块。Python 3.14 起POSIX 平台默认启动方式也不再是fork而是更安全的forkserver。所以多进程适合“每个任务计算量足够大”的场景。如果任务很小序列化和调度成本可能把收益吃掉。面试回答可以这样说CPU 密集型任务想利用多核默认 CPython 下优先考虑多进程或释放 GIL 的原生扩展。多进程能绕开 GIL但会带来进程启动、序列化、进程间通信和跨平台启动方式差异所以不适合特别细碎的小任务。五、asyncio适合高并发 IO但它不是多线程asyncio是 Python 标准库里的异步 IO 框架。官方文档把它定义为用async/await语法编写并发代码的库适合 IO 密集和高层网络代码。它的核心组件可以这样理解概念一句话理解coroutineasync def调用后得到的协程对象Task被事件循环调度的协程任务event loop负责调度任务、处理 IO 事件的循环await当前协程主动让出控制权等待结果回来最小例子importasyncioasyncdeffetch_user(user_id:int):awaitasyncio.sleep(1)return{id:user_id}asyncdefmain():usersawaitasyncio.gather(fetch_user(1),fetch_user(2),fetch_user(3),)print(users)asyncio.run(main())这段代码不是开了三个线程。更准确地说event loop 开始运行 - task 1 遇到 await暂停 - event loop 切到 task 2 - task 2 遇到 await暂停 - event loop 切到 task 3 - IO 结果回来后恢复对应 task官方asyncio.Task文档里也强调事件循环使用协作式调度一次运行一个 Task当某个 Task 等待 Future 完成时事件循环运行其他 Task、回调或执行 IO 操作。协程和线程的关键区别线程由操作系统调度切换点不完全由你控制。共享内存时要特别注意竞态条件。协程由事件循环调度通常在await处主动让出控制权。它不是抢占式切换而是协作式切换。这带来两个结果第一协程很适合大量 IO 等待任务。比如一个服务同时维护很多 HTTP 请求、WebSocket 连接、SSE 流式输出只要底层库支持异步 IO协程可以用较少线程处理大量连接。第二协程怕阻塞。如果你在async def里写了阻塞代码事件循环就被卡住了。反例importtimeasyncdefbad_handler():time.sleep(5)# 阻塞整个事件循环returndone正确方向importasyncioasyncdefgood_handler():awaitasyncio.sleep(5)returndone如果必须调用同步阻塞函数可以考虑importasynciodefblocking_io():# 调用同步数据库客户端、同步 SDK、老接口returnresultasyncdefhandler():resultawaitasyncio.to_thread(blocking_io)returnresult但这不是让阻塞函数“变异步”而是把它挪到线程里跑避免卡住事件循环。六、FastAPI 里怎么选def和async def这部分是 Python 并发面试和实际后端项目最容易混的地方。FastAPI 官方文档给的判断很实用如果你调用的库支持await路径函数用async def。如果你调用的是同步阻塞库比如普通数据库客户端、文件系统、同步 HTTP SDK可以用普通def。如果不知道怎么选用普通def更稳。def和async def可以混用FastAPI 会按不同方式处理。关键机制是FastAPI 底层基于 Starlette。Starlette 文档说明同步 endpoint 会在线程池里运行用来避免阻塞事件循环。FastAPI 文档也提到普通defpath operation 会在外部线程池执行并被 await。这意味着fromfastapiimportFastAPIimportrequests appFastAPI()app.get(/sync-user)defsync_user():resprequests.get(https://example.com/user,timeout3)returnresp.json()这种同步阻塞代码写成def时FastAPI 会把它放到线程池不会直接堵住事件循环。但下面这种写法就危险fromfastapiimportFastAPIimportrequests appFastAPI()app.get(/bad-user)asyncdefbad_user():resprequests.get(https://example.com/user,timeout3)returnresp.json()requests.get()是同步阻塞调用。你把函数写成async def并不会让requests变成异步反而可能在事件循环线程里直接阻塞。如果用异步客户端才适合async deffromfastapiimportFastAPIimporthttpx appFastAPI()app.get(/async-user)asyncdefasync_user():asyncwithhttpx.AsyncClient()asclient:respawaitclient.get(https://example.com/user)returnresp.json()所以判断不是看“我想不想高并发”而是看调用链调用链里都是可 await 的异步 IO - async def 调用链里有同步阻塞库 - def 或显式丢到线程池 调用链里是 CPU 密集任务 - 不要靠 async def考虑进程池、任务队列或专门计算服务七、四种模型怎么选可以用这张表做快速判断。场景推荐方向原因少量阻塞 IO代码以同步库为主threading/ThreadPoolExecutor改造成本低等待期间可以切换高并发网络 IO库支持 asyncasyncio/ ASGI / FastAPIasync def用事件循环管理大量连接CPU 密集纯 Python 计算multiprocessing/ProcessPoolExecutor绕开默认 CPython GIL利用多核CPU 密集但由 NumPy 等原生库完成先看库是否释放 GILC/原生库可能已经并行或释放 GILFastAPI 调同步数据库/同步 SDK普通def或线程池避免阻塞事件循环FastAPI 调异步 DB/HTTP 客户端async defawait把等待点交给事件循环长耗时后台任务任务队列 / 进程池 / 独立 worker不占用 Web 请求生命周期再压缩成一句话IO 等待多用线程或 asyncio连接数高且库支持异步用 asyncioCPU 计算重用多进程或原生计算库FastAPI 里不要把同步阻塞代码伪装成async def。八、面试高频追问1. Python 多线程真的没用吗不能这么说。更准确的回答是默认 CPython 里GIL 限制同一进程内多个线程并行执行 Python 字节码所以纯 Python CPU 密集任务不适合用多线程提速。但 IO 阻塞时 GIL 可以释放多线程仍然适合网络请求、文件读写、同步 SDK 调用等 IO 密集场景。2. 线程和协程有什么区别线程是操作系统调度的执行单元共享进程内存切换点不完全由程序控制。协程通常由事件循环调度在await处主动让出控制权是协作式调度。面试可以这样答线程更适合同步阻塞 IO 的并发处理协程更适合支持异步 IO 的高并发网络场景。协程不是线程也不是自动并行它依赖代码在 await 点让出控制权。3. asyncio 为什么不能加速 CPU 密集任务因为 asyncio 的优势在等待 IO 时切换任务。CPU 密集代码如果一直计算、不await事件循环没有机会调度其他任务。这种代码写在async def里也没用asyncdefcpu_task():total0foriinrange(10_000_000):totali*ireturntotal它会一直占着事件循环线程跑计算。4. 多进程为什么能利用多核因为多个进程有独立解释器和独立 GIL可以被操作系统调度到不同 CPU 核上并行执行。但多进程需要序列化参数和结果不适合超小任务也不适合频繁传递巨大对象。5. FastAPI 里async def一定更快吗不一定。如果函数内部都是异步 IO并且每个慢操作都能awaitasync def很适合。如果函数内部调用同步阻塞库写成async def反而可能堵住事件循环。FastAPI 对普通defendpoint 会使用线程池处理同步代码这在很多老 SDK 或同步数据库客户端场景里更稳。6.await到底做了什么await不是开线程也不是让代码并行。它表示当前协程等待一个 awaitable 的结果期间把控制权交还给事件循环让事件循环调度其他任务。等结果回来后当前协程再从暂停点继续执行。7.asyncio.gather()是并行吗通常不是 CPU 并行。它会把多个 awaitable 一起调度让它们在 IO 等待期间并发推进。如果这些任务都是网络请求效果很好如果它们都是纯 Python CPU 大循环仍然会卡住事件循环。九、最容易踩的 5 个坑坑 1看到 GIL 就否定线程线程在 CPU 密集任务上确实受限但在 IO 密集任务上仍然有价值。更好的问题不是“线程有没有用”而是我的线程大部分时间是在算还是在等坑 2把async def当成性能开关async def只是声明协程函数。真正能释放事件循环的是await一个非阻塞 IO。如果内部全是同步阻塞调用async def只是把阻塞放进了事件循环线程。坑 3在事件循环里写time.sleep()time.sleep()会阻塞当前线程。事件循环就在这个线程里时所有协程都会被拖住。异步代码里应该用awaitasyncio.sleep(1)坑 4用进程池处理很碎的小任务进程池有启动、调度、序列化成本。任务太小成本可能比计算本身还高。CPU 密集任务用进程池前最好把任务粒度合并到足够大。坑 5FastAPI 里混用 sync/async 不看调用链项目里最常见的问题是接口写成async def里面却调同步数据库客户端、同步 HTTP SDK、同步文件操作。判断标准只有一个慢操作能不能await不能的话就不要假装它是异步。十、总结先判断瓶颈再选择模型Python 并发不要从“哪个技术更高级”开始而要从任务瓶颈开始。最后用一张判断表收尾任务主要在等网络 / DB / 文件 同步库多 - 线程 / def endpoint 异步库成熟 - asyncio / async def 任务主要在吃 CPU 纯 Python 计算 - 多进程 / 进程池 原生计算库 - 看库是否释放 GIL 或自带并行 Web 接口 async def 只适合真正可 await 的调用链 def endpoint 可以承接同步阻塞代码 CPU 重活不要放在请求线程里硬跑面试里可以用一句话概括默认 CPython 的 GIL 限制多线程并行执行 Python 字节码所以 CPU 密集任务通常用多进程或原生扩展IO 密集任务因为会等待外部资源线程和 asyncio 都有价值。线程适合同步阻塞库asyncio 适合支持 await 的高并发 IO。FastAPI 里要按调用链选择def或async def不要把阻塞代码写进事件循环。把这条线讲清楚Python 并发八股就不是背概念了而是能落到后端接口、任务队列、性能排查和 FastAPI 项目设计里的工程判断。参考资料Python 官方文档threading - Thread-based parallelismhttps://docs.python.org/3/library/threading.htmlPython 官方文档Thread states and the global interpreter lockhttps://docs.python.org/3.14/c-api/threads.htmlPython 官方文档multiprocessing - Process-based parallelismhttps://docs.python.org/3/library/multiprocessing.htmlPython 官方文档concurrent.futures - Launching parallel taskshttps://docs.python.org/3/library/concurrent.futures.htmlPython 官方文档asyncio - Asynchronous I/Ohttps://docs.python.org/3/library/asyncio.htmlPython 官方文档Coroutines and Taskshttps://docs.python.org/3/library/asyncio-task.htmlPython 官方文档Python support for free threadinghttps://docs.python.org/3/howto/free-threading-python.htmlPEP 703 - Making the Global Interpreter Lock Optional in CPythonhttps://peps.python.org/pep-0703/FastAPI 官方文档Concurrency and async / awaithttps://fastapi.tiangolo.com/async/Starlette 官方文档Thread Poolhttps://starlette.dev/threadpool/
Python 并发八股:线程、进程、协程和 asyncio 到底怎么选?
发布时间:2026/6/7 22:09:16
Python 并发八股线程、进程、协程和 asyncio 到底怎么选开场Python 并发不是一句“有 GIL所以多线程没用”前两篇我们先把 Python 的对象、引用、装饰器、生成器和上下文管理器讲完了。今天继续补一个后端面试绕不开的问题Python 有 GIL多线程是不是没用IO 密集任务到底用线程还是 asyncioCPU 密集任务为什么推荐多进程协程和线程到底差在哪里async def写了以后代码就一定不会阻塞吗FastAPI 里为什么有时建议写def有时又建议写async def很多回答会停在一句话Python 有 GIL所以多线程不能利用多核。这句话不算错但太粗了。真正能过面试、能落项目的回答至少要补上三层判断你的任务瓶颈是 CPU 计算还是网络、文件、数据库等待你想要的是并发处理更多等待任务还是并行压满多个 CPU 核你用的库是同步阻塞库还是支持await的异步库所以今天这篇不按 API 清单讲而是按选择题来讲Python 并发模型的核心不是“线程、进程、协程谁更高级”而是先判断任务瓶颈再选择执行单元和调度方式。一、先分清并发和并行先把两个词拆开。并发强调“同时处理多个任务的能力”。它不要求同一时刻真的有多个 CPU 核在执行代码。一个线程也可以通过事件循环在多个等待任务之间切换。并行强调“同一时刻真的有多个执行单元在运行”。比如多个 CPU 核同时跑计算任务。用后端请求举例并发 一个服务同时处理 1000 个请求 其中大部分时间在等网络、数据库、Redis 并行 多个 CPU 核同时计算图片压缩、特征提取、加密、模型推理这就是为什么 Web 后端经常更关心并发而数据计算更关心并行。如果任务大部分时间在等 IO你需要的是“等待期间别闲着”。线程和 asyncio 都可能有价值。如果任务大部分时间都在吃 CPU你需要的是“多个核一起算”。默认 CPython 里多进程通常比多线程更直接。二、GIL它限制的是同一进程内多个线程执行 Python 字节码GIL 全称是 Global Interpreter Lock全局解释器锁。Python 官方 C API 文档对它的描述很直接在常规 CPython 构建里线程访问 Python 对象前必须持有 GIL同一时刻通常只有持有 GIL 的线程能操作 Python 对象或调用 Python C API。翻译成面试语言在默认 CPython 里同一个进程内多个 Python 线程不能同时并行执行 Python 字节码。这句话有三个限定词很重要。第一是默认 CPython。Python 是语言CPython 是最常见的解释器实现。我们日常说的 GIL通常是在说 CPython 的实现细节。第二是同一个进程内的多个线程。多进程不是共享同一个解释器状态每个进程有自己的 Python 解释器和 GIL所以可以利用多个 CPU 核。第三是执行 Python 字节码。阻塞 IO 时GIL 可以释放。官方文档也明确提到文件读写这类阻塞 IO 周围会释放 GIL。所以 IO 密集任务里多线程仍然可能有价值。这也是“Python 多线程没用”这句话的问题它把 CPU 密集和 IO 密集混在一起了。GIL 为什么存在最常见的解释是CPython 的对象模型和引用计数需要线程安全。假设两个线程同时操作同一个对象的引用计数如果没有同步保护最简单的加一减一都可能出错。GIL 用一把大锁保护解释器内部状态实现成本更低也让大量 C 扩展和内置对象的行为更容易维护。代价就是CPU 密集型纯 Python 多线程不能线性利用多核。Python 3.13 以后的 free-threading 怎么看从 Python 3.13 开始CPython 支持一种可选的 free-threaded 构建可以禁用 GIL。PEP 703 也已经被接受。但面试和大多数生产项目里回答不能直接说“以后没有 GIL 了”。更稳的说法是CPython 已经在推进可选 free-threaded 构建但常规环境和大量三方包仍然要按默认 GIL 模型理解。写并发代码时依然要先区分 IO 密集、CPU 密集和库是否线程安全。这能体现你知道新变化也不会把实验性或可选能力说成所有生产环境的默认现实。三、线程适合 IO 等待不适合用来硬扛 CPU 计算Python 的threading模块提供线程级并发。线程在同一个进程里运行内存空间共享所以传递对象很方便。线程适合这些场景同时发多个 HTTP 请求。同时读写多个文件。后台跑一些轻量阻塞任务。调用没有 async 版本的阻塞库。在 FastAPI / Starlette 里把同步函数放到线程池避免堵住事件循环。线程不适合这些场景纯 Python 大循环计算。图片、视频、压缩、加密这类 CPU 密集任务。希望多个线程同时压满多个 CPU 核的计算任务。一个典型线程池写法fromconcurrent.futuresimportThreadPoolExecutor,as_completedimportrequestsdeffetch(url:str)-tuple[str,int]:resprequests.get(url,timeout3)returnurl,resp.status_code urls[https://docs.python.org/3/library/threading.html,https://docs.python.org/3/library/asyncio.html,https://docs.python.org/3/library/multiprocessing.html,]withThreadPoolExecutor(max_workers8)aspool:futures[pool.submit(fetch,url)forurlinurls]forfutureinas_completed(futures):print(future.result())这个例子里线程的价值不是让 Python 代码并行计算而是一个请求在等网络响应时其他线程可以继续发请求或处理返回结果。线程最大的坑共享状态线程共享进程内存这既是优点也是坑。看一个危险写法counter0defincr():globalcounterfor_inrange(10000):counter1counter 1看起来是一句底层并不是不可分割的原子操作。多个线程同时改共享变量需要锁、队列或者尽量避免共享可变状态。更稳的方式是fromqueueimportQueuefromthreadingimportThread queueQueue()defworker():whileTrue:itemqueue.get()try:handle(item)finally:queue.task_done()面试里如果只说“线程适合 IO 密集”还不够。最好补一句线程共享内存通信方便但共享可变状态需要同步控制常见做法是用 Lock、Queue 或减少共享状态。四、进程适合 CPU 密集但要付出通信和启动成本multiprocessing走的是进程级并发。官方文档也说得很清楚它通过子进程绕开 GIL让程序可以利用多个处理器。所以 CPU 密集任务优先考虑多进程批量图片处理。文本特征提取。CPU 计算型数据清洗。压缩、加密、解析大文件。不依赖 GPU 的本地推理或预处理。典型写法fromconcurrent.futuresimportProcessPoolExecutordefis_prime(n:int)-bool:ifn2:returnFalseforiinrange(2,int(n**0.5)1):ifn%i0:returnFalsereturnTruenumbers[112272535095293,112582705942171,115280095190773]withProcessPoolExecutor()aspool:forresultinpool.map(is_prime,numbers):print(result)多进程为什么能绕开 GIL因为每个进程都有自己的解释器、自己的内存空间、自己的 GIL。多个进程可以被操作系统调度到不同 CPU 核上真正并行运行。多进程的代价多进程不是“比线程高级”它只是解决了另一类问题。它的成本很明确进程启动比线程重。进程之间不共享普通内存。参数和返回值通常需要序列化。函数、参数、返回结果要能被 pickle。Windows 和 macOS 默认使用spawn子进程需要重新导入模块。Python 3.14 起POSIX 平台默认启动方式也不再是fork而是更安全的forkserver。所以多进程适合“每个任务计算量足够大”的场景。如果任务很小序列化和调度成本可能把收益吃掉。面试回答可以这样说CPU 密集型任务想利用多核默认 CPython 下优先考虑多进程或释放 GIL 的原生扩展。多进程能绕开 GIL但会带来进程启动、序列化、进程间通信和跨平台启动方式差异所以不适合特别细碎的小任务。五、asyncio适合高并发 IO但它不是多线程asyncio是 Python 标准库里的异步 IO 框架。官方文档把它定义为用async/await语法编写并发代码的库适合 IO 密集和高层网络代码。它的核心组件可以这样理解概念一句话理解coroutineasync def调用后得到的协程对象Task被事件循环调度的协程任务event loop负责调度任务、处理 IO 事件的循环await当前协程主动让出控制权等待结果回来最小例子importasyncioasyncdeffetch_user(user_id:int):awaitasyncio.sleep(1)return{id:user_id}asyncdefmain():usersawaitasyncio.gather(fetch_user(1),fetch_user(2),fetch_user(3),)print(users)asyncio.run(main())这段代码不是开了三个线程。更准确地说event loop 开始运行 - task 1 遇到 await暂停 - event loop 切到 task 2 - task 2 遇到 await暂停 - event loop 切到 task 3 - IO 结果回来后恢复对应 task官方asyncio.Task文档里也强调事件循环使用协作式调度一次运行一个 Task当某个 Task 等待 Future 完成时事件循环运行其他 Task、回调或执行 IO 操作。协程和线程的关键区别线程由操作系统调度切换点不完全由你控制。共享内存时要特别注意竞态条件。协程由事件循环调度通常在await处主动让出控制权。它不是抢占式切换而是协作式切换。这带来两个结果第一协程很适合大量 IO 等待任务。比如一个服务同时维护很多 HTTP 请求、WebSocket 连接、SSE 流式输出只要底层库支持异步 IO协程可以用较少线程处理大量连接。第二协程怕阻塞。如果你在async def里写了阻塞代码事件循环就被卡住了。反例importtimeasyncdefbad_handler():time.sleep(5)# 阻塞整个事件循环returndone正确方向importasyncioasyncdefgood_handler():awaitasyncio.sleep(5)returndone如果必须调用同步阻塞函数可以考虑importasynciodefblocking_io():# 调用同步数据库客户端、同步 SDK、老接口returnresultasyncdefhandler():resultawaitasyncio.to_thread(blocking_io)returnresult但这不是让阻塞函数“变异步”而是把它挪到线程里跑避免卡住事件循环。六、FastAPI 里怎么选def和async def这部分是 Python 并发面试和实际后端项目最容易混的地方。FastAPI 官方文档给的判断很实用如果你调用的库支持await路径函数用async def。如果你调用的是同步阻塞库比如普通数据库客户端、文件系统、同步 HTTP SDK可以用普通def。如果不知道怎么选用普通def更稳。def和async def可以混用FastAPI 会按不同方式处理。关键机制是FastAPI 底层基于 Starlette。Starlette 文档说明同步 endpoint 会在线程池里运行用来避免阻塞事件循环。FastAPI 文档也提到普通defpath operation 会在外部线程池执行并被 await。这意味着fromfastapiimportFastAPIimportrequests appFastAPI()app.get(/sync-user)defsync_user():resprequests.get(https://example.com/user,timeout3)returnresp.json()这种同步阻塞代码写成def时FastAPI 会把它放到线程池不会直接堵住事件循环。但下面这种写法就危险fromfastapiimportFastAPIimportrequests appFastAPI()app.get(/bad-user)asyncdefbad_user():resprequests.get(https://example.com/user,timeout3)returnresp.json()requests.get()是同步阻塞调用。你把函数写成async def并不会让requests变成异步反而可能在事件循环线程里直接阻塞。如果用异步客户端才适合async deffromfastapiimportFastAPIimporthttpx appFastAPI()app.get(/async-user)asyncdefasync_user():asyncwithhttpx.AsyncClient()asclient:respawaitclient.get(https://example.com/user)returnresp.json()所以判断不是看“我想不想高并发”而是看调用链调用链里都是可 await 的异步 IO - async def 调用链里有同步阻塞库 - def 或显式丢到线程池 调用链里是 CPU 密集任务 - 不要靠 async def考虑进程池、任务队列或专门计算服务七、四种模型怎么选可以用这张表做快速判断。场景推荐方向原因少量阻塞 IO代码以同步库为主threading/ThreadPoolExecutor改造成本低等待期间可以切换高并发网络 IO库支持 asyncasyncio/ ASGI / FastAPIasync def用事件循环管理大量连接CPU 密集纯 Python 计算multiprocessing/ProcessPoolExecutor绕开默认 CPython GIL利用多核CPU 密集但由 NumPy 等原生库完成先看库是否释放 GILC/原生库可能已经并行或释放 GILFastAPI 调同步数据库/同步 SDK普通def或线程池避免阻塞事件循环FastAPI 调异步 DB/HTTP 客户端async defawait把等待点交给事件循环长耗时后台任务任务队列 / 进程池 / 独立 worker不占用 Web 请求生命周期再压缩成一句话IO 等待多用线程或 asyncio连接数高且库支持异步用 asyncioCPU 计算重用多进程或原生计算库FastAPI 里不要把同步阻塞代码伪装成async def。八、面试高频追问1. Python 多线程真的没用吗不能这么说。更准确的回答是默认 CPython 里GIL 限制同一进程内多个线程并行执行 Python 字节码所以纯 Python CPU 密集任务不适合用多线程提速。但 IO 阻塞时 GIL 可以释放多线程仍然适合网络请求、文件读写、同步 SDK 调用等 IO 密集场景。2. 线程和协程有什么区别线程是操作系统调度的执行单元共享进程内存切换点不完全由程序控制。协程通常由事件循环调度在await处主动让出控制权是协作式调度。面试可以这样答线程更适合同步阻塞 IO 的并发处理协程更适合支持异步 IO 的高并发网络场景。协程不是线程也不是自动并行它依赖代码在 await 点让出控制权。3. asyncio 为什么不能加速 CPU 密集任务因为 asyncio 的优势在等待 IO 时切换任务。CPU 密集代码如果一直计算、不await事件循环没有机会调度其他任务。这种代码写在async def里也没用asyncdefcpu_task():total0foriinrange(10_000_000):totali*ireturntotal它会一直占着事件循环线程跑计算。4. 多进程为什么能利用多核因为多个进程有独立解释器和独立 GIL可以被操作系统调度到不同 CPU 核上并行执行。但多进程需要序列化参数和结果不适合超小任务也不适合频繁传递巨大对象。5. FastAPI 里async def一定更快吗不一定。如果函数内部都是异步 IO并且每个慢操作都能awaitasync def很适合。如果函数内部调用同步阻塞库写成async def反而可能堵住事件循环。FastAPI 对普通defendpoint 会使用线程池处理同步代码这在很多老 SDK 或同步数据库客户端场景里更稳。6.await到底做了什么await不是开线程也不是让代码并行。它表示当前协程等待一个 awaitable 的结果期间把控制权交还给事件循环让事件循环调度其他任务。等结果回来后当前协程再从暂停点继续执行。7.asyncio.gather()是并行吗通常不是 CPU 并行。它会把多个 awaitable 一起调度让它们在 IO 等待期间并发推进。如果这些任务都是网络请求效果很好如果它们都是纯 Python CPU 大循环仍然会卡住事件循环。九、最容易踩的 5 个坑坑 1看到 GIL 就否定线程线程在 CPU 密集任务上确实受限但在 IO 密集任务上仍然有价值。更好的问题不是“线程有没有用”而是我的线程大部分时间是在算还是在等坑 2把async def当成性能开关async def只是声明协程函数。真正能释放事件循环的是await一个非阻塞 IO。如果内部全是同步阻塞调用async def只是把阻塞放进了事件循环线程。坑 3在事件循环里写time.sleep()time.sleep()会阻塞当前线程。事件循环就在这个线程里时所有协程都会被拖住。异步代码里应该用awaitasyncio.sleep(1)坑 4用进程池处理很碎的小任务进程池有启动、调度、序列化成本。任务太小成本可能比计算本身还高。CPU 密集任务用进程池前最好把任务粒度合并到足够大。坑 5FastAPI 里混用 sync/async 不看调用链项目里最常见的问题是接口写成async def里面却调同步数据库客户端、同步 HTTP SDK、同步文件操作。判断标准只有一个慢操作能不能await不能的话就不要假装它是异步。十、总结先判断瓶颈再选择模型Python 并发不要从“哪个技术更高级”开始而要从任务瓶颈开始。最后用一张判断表收尾任务主要在等网络 / DB / 文件 同步库多 - 线程 / def endpoint 异步库成熟 - asyncio / async def 任务主要在吃 CPU 纯 Python 计算 - 多进程 / 进程池 原生计算库 - 看库是否释放 GIL 或自带并行 Web 接口 async def 只适合真正可 await 的调用链 def endpoint 可以承接同步阻塞代码 CPU 重活不要放在请求线程里硬跑面试里可以用一句话概括默认 CPython 的 GIL 限制多线程并行执行 Python 字节码所以 CPU 密集任务通常用多进程或原生扩展IO 密集任务因为会等待外部资源线程和 asyncio 都有价值。线程适合同步阻塞库asyncio 适合支持 await 的高并发 IO。FastAPI 里要按调用链选择def或async def不要把阻塞代码写进事件循环。把这条线讲清楚Python 并发八股就不是背概念了而是能落到后端接口、任务队列、性能排查和 FastAPI 项目设计里的工程判断。参考资料Python 官方文档threading - Thread-based parallelismhttps://docs.python.org/3/library/threading.htmlPython 官方文档Thread states and the global interpreter lockhttps://docs.python.org/3.14/c-api/threads.htmlPython 官方文档multiprocessing - Process-based parallelismhttps://docs.python.org/3/library/multiprocessing.htmlPython 官方文档concurrent.futures - Launching parallel taskshttps://docs.python.org/3/library/concurrent.futures.htmlPython 官方文档asyncio - Asynchronous I/Ohttps://docs.python.org/3/library/asyncio.htmlPython 官方文档Coroutines and Taskshttps://docs.python.org/3/library/asyncio-task.htmlPython 官方文档Python support for free threadinghttps://docs.python.org/3/howto/free-threading-python.htmlPEP 703 - Making the Global Interpreter Lock Optional in CPythonhttps://peps.python.org/pep-0703/FastAPI 官方文档Concurrency and async / awaithttps://fastapi.tiangolo.com/async/Starlette 官方文档Thread Poolhttps://starlette.dev/threadpool/