1. 本课定位是什么、为何重要上一课我们学会了用 requests 稳稳地调外部接口会设超时、会看状态码、会把密钥放进环境变量。对「只调一两次」的脚本来说那样已经够用了。可是真实工作里你常常要连续访问很多个地址——比如一次同步二十个部门的数据、批量检查一批网址是否存活。如果仍然一个请求完全结束再发下一个等待时间会像排队一样越加越长每个接口干等一秒十个接口就接近十秒以上人还没写复杂业务光等网络就把时间耗光了。所以这节课一开始不急着甩线程、进程这些名词而是先用大白话把「并发」放到你已经会的能力后面它要解决的是「多个等待能不能叠在一起做」的问题。学完你应该能说清什么叫阻塞、什么叫并发和并行、以及为什么我们要专门开一课来对比不同做法。地图先画清楚后面的表格和代码才不会变成死记硬背。第 34 课的requests.get默认同步阻塞一个请求不结束下一行代码不会跑。若每个请求干等约 1 秒串行 3 个就要约 3 秒以上——时间在相加。概念一句话并发concurrency多个任务交替推进总进度重叠并行parallelism多核上真的同时算进程更易吃到阻塞当前调用不返回后面的代码动不了本课目标会判断 IO/CPU会在线程 / 进程 / 协程里选型为何重要调多个 API、批量下载、后台计算选错模型会「又慢又容易写崩」。对比已学Day34 默认同步客户端本课一次只安心等一个响应多个等待能否重叠代码直观、好调试要处理共享数据、GIL、开销适合简单脚本适合批量 IO / 重 CPU 分流生活类比做法像什么对应串行一次只煮一壶水壶好了再煮下一壶一个get接一个get并发线程/协程三壶一起烧谁先响谁关多个请求同时等网络并行多进程请了三个厨子各管一灶多核同时算压缩/矩阵2. 本质IO 密集 vs CPU 密集很多人一听说「程序慢、要并发」第一反应就是那我多开几个线程吧。这有点像一生病就吃同一种药。其实你得先分清慢是因为程序在干等——等网络回包、等磁盘读完、等数据库响应——还是因为 CPU 一直在满负荷计算比如压缩图片、跑很大的循环。这两种慢对应的办法完全不同。上一节我们已经知道「一个接一个请求会把等待时间加起来」。这一节继续往下挖一层在动手开线程或开进程之前先学会看病分科。你会看到 IO 密集和 CPU 密集的对照表、几条快速判断方法以及为什么在 Python 里「多线程算得快」常常不成立但「多线程等网络」却经常管用。分科对了后面选型才不会南辕北辙。先分「时间主要花在哪」再谈开线程还是开进程。类型时间主要花在例子并发思路IO 密集等网络/磁盘/数据库爬网页、调 API、读大文件重叠等待线程或协程CPU 密集算满 CPU压缩图片、加密、大循环多进程或多核数值库混合又等又算下载后再压缩分段IO 用线程/协程CPU 用进程池怎么快速判断把任务单独跑一遍看 CPU 占用长期接近 100% → 偏 CPUCPU 很闲但很慢 → 偏 IO。任务主体是不是await/recv/read/get这类「等」→ IO。任务主体是不是双重循环、图像卷积、纯 Python 大计算 → CPU。GIL一句人话CPython 里多线程对「纯 Python 死算」加速有限——同一时刻多半只有一个线程在执行 Python 字节码。但线程在等 IO时会释放机会所以网络请求用线程池仍然经常明显变快。约束与坑坑现象正确直觉无脑开 100 线程变慢、被封、内存涨池化、限并发如 520多线程改同一变量结果随机错锁 / 队列 / 少共享CPU 任务用纯多线程加速不明显换进程或 NumPy 等协程里time.sleep/requests事件循环卡住用 async 库第 36 课进程间传超大对象启动慢、内存翻倍少传数据传路径/id以为并发一定更快任务极短时更慢调度本身有开销3. 三种模型对照总表分清程序是「在等」还是「在算」之后下一步才是打开工具箱。Python 里和并发相关、入门最常听到的大致是三件套多线程、多进程、协程。名字听着硬生活里都能找到影子多线程像同一间厨房里几个跑腿的交替干活多进程像再请几个厨子各管一灶协程像一个人同时盯着很多壶水哪壶开了管哪壶。这一节先不陷进大段代码而是用总表把三件套的「像什么、适合什么、要注意什么、入门从哪个 API 进」摆在一起让你脑中有一张地图。细节可以后练但地图必须先有——否则后面一上来写 ThreadPoolExecutor你并不知道它在解决哪一类问题和进程、协程又有什么分工。模型像什么更适合注意入门入口多线程一屋多人交替干活大量 IO 等待GIL共享要小心ThreadPoolExecutor多进程多个独立工人CPU 重计算内存与通信成本高ProcessPoolExecutor协程单线程协作切换高并发网络 IO勿塞阻塞调用asyncio第 36 课3.1 线程再展开把镜头推近到三件套里的第一件多线程。你可以先把它理解成「同一个程序里雇了好几个跑腿的」。当其中一个在门口等外卖等网络响应时别的跑腿可以去送别的单而不用所有人在门口排成一条队干等。它特别值得你现在学是因为和上一课的 requests 非常合拍requests 是同步库线程池能直接把它丢进去跑迁移成本最低。所以本课综合实验会以线程池为主。你也会看到它的缺点共享数据容易写乱、线程不是越多越快、纯计算场景加速有限。先建立这些直觉用起来才心里有数。优点API 相对好懂适合 IO与现有同步库如requests好配合。缺点共享内存易写错线程数不是越多越好CPU 密集收益有限。fromconcurrent.futuresimportThreadPoolExecutordefwork(i):returni*iwithThreadPoolExecutor(max_workers4)asex:print(list(ex.map(work,range(5))))预期输出[0, 1, 4, 9, 16]3.2 进程再展开第二件工具是多进程。如果说线程是同一屋檐下的多个跑腿进程更像「再请几个独立工人每人一台电脑」。彼此内存不随便混用在多核机器上更适合干重活批量压缩、大计算、CPU 打满的那种任务。代价也明显请人贵——启动慢、占内存、数据传来传去也麻烦。所以本课不会强迫你跑很重的压测但必须在地图上标清楚如果瓶颈是算力而不是网络别只会死命加线程。后面会有一段说明性的进程池示例读懂边界即可忙的话可以先不跑。优点可真正利用多核做 CPU 活。缺点启动贵参数/返回值要能序列化Windows 上要注意if __name__ __main__。适用批量图片缩放、纯 Python 重计算、CPU 型数据清洗。本课不强制跑重 CPU 压测但选型时必须知道这条路。3.3 协程再展开预告第三件是协程。它常常被说得很玄你可以先用一句大白话一个人同时盯着很多壶水哪壶开了管哪壶而不用真的雇一堆人。在大量网络等待的场景里这种「协作式切换」往往很轻、很合适。写法上会看到 async、await生态也要求库支持异步如果在协程里仍去调用阻塞的 requests 或 time.sleep就等于盯水的人自己发呆站桩所有壶都没人管。细节和下一课的 httpx 实战绑定这里只先让它在地图上有个位置和线程一样擅长 IO但模型和注意事项不同。优点单线程内可挂起大量等待中的网络任务切换成本低。缺点生态要 async一处阻塞会拖死全体。细节与httpx实战 → 第 36 课。3.4 能力分类不背全部 API工具名字认识了下一个坑是一打开文档就想把 threading、multiprocessing 的 API 全背下来。没必要。入门阶段你真正要会的是几类「能力」怎样提交一批任务、怎样拿回结果、CPU 重活该找进程池、协程相关下节再专攻、以及为什么共享数据危险。这一小节用表格把能力归归类对应到常见入口函数。目的不是让你默写签名而是让你需要时知道「该去哪个抽屉找工具」。和前面学 requests 时把能力分成发送、稳健、会话、身份是同一个思路分类比罗列更上脑。类别常见入口本课深度提交一批 IO 任务ThreadPoolExecutor.map/submit实践重点等结果list(ex.map(...))、Future.result()会用即可CPU 任务ProcessPoolExecutor边界说明协程async/await、gather下节专攻同步原语Lock、Queue只建立「为何需要」4. 场景选型表多练几道「口算」光有地图还不够得拿去「看病开药」。这一节给你一串很像真实工作的场景爬很多网页、同时调几个用户接口、压缩一千张图、算大矩阵、写很多小文件、维持海量空闲连接、以及「脚本其实只请求一次」。你要练的是脱口而出这个继续串行就行还是上线程/协程还是上进程。如果前面 IO 与 CPU 的分科你听懂了这里会非常顺如果卡住了说明分科还要再回看一眼。文后有口诀和一张决策小流程方便你复习时当小抄。记住任务很少时不上并发往往才是更稳的选择。任务更合适原因爬 100 个网页线程或协程主要等网络同时调 3 个用户资料 API线程或协程同上压缩 1000 张大图多进程 / 专业库CPU 重大矩阵运算多进程或 NumPyCPU / 本地优化写很多小文件且主要等磁盘线程/协程也可IO 等待每秒上万空闲连接协程更常见线程栈成本高脚本只请求 1 次 API串行即可不必上并发口诀IO 密集 → 线程或协程CPU 密集 → 进程或释放 GIL 的库任务极少 → 先串行。决策小流程任务数量是否很少 --是-- 串行 | 否 v 主要是等网络/磁盘 --是-- 线程池 或 协程 | 否主要是算 v 多进程 / 专用数值库5. 共享数据为何会乱并发能省时间但不是免费午餐。一旦多个线程同时去改同一块数据就可能出现「明明加了两次结果却像只加了一次」的怪事。你可以想象两个人同时改同一份表格、又互不通气最后表上的数字对不上。上一节我们鼓励你用线程池去重叠网络等待这一节立刻补上安全边界重叠等待没问题重叠着乱改共享变量就有问题。我们会用一个极简的 balance 加减故事说明「会乱」并给出少共享、用队列、慎用锁、返回新结果再汇总等方向。入门只要求建立直觉不要求你马上写出复杂的锁代码——先别踩坑比先炫技重要。线程A: 读 balance0 → 加1 → 写回 线程B: 读 balance0 → 加1 → 写回 结果可能仍是 1而不是 2修改前单线程balance 1可预期。多线程无保护后可能丢更新。策略说明入门建议少共享各线程算各的最后汇总优先队列任务进Queue工人只取任务常用锁with lock:包住临界区能少用少用不可变结果只返回新对象不改全局 list 原地推荐# 不推荐多线程同时 append 同一 list 且无锁可能出问题# 推荐map 返回结果主线程再 list() 汇总本课建立直觉即可不必一上来写复杂锁。6. 串行 vs 线程时间账怎么算马上要做计时实验了。很多人一跑完就纠结教材上说大概 3 秒和 1 秒我怎么跑出 9 秒和 3 秒是不是程序写错了这一节先帮你算一笔「理想账」如果每个请求纯等待一秒、本地计算可忽略串行是时间相加多线程同时等则接近「最慢那个」而不是相加。然后再说明真实公网还有 DNS、建连接、服务端排队、限流和抖动秒数几乎一定比理想值「胖一圈」。所以实验看的是趋势——线程是否明显快于串行——而不是死磕某一个精确数字。看懂这笔账你才不会被输出里的 seconds 带着满地跑。假设 3 个请求每个「纯等待」约 1 秒忽略本地计算方式理想墙钟时间说明串行≈ 111 3s等待相加3 线程同时等≈ max(1,1,1) ≈ 1s等待重叠现实公网常大于理想值DNS、建连、限流、抖动所以实验里看到「串行 9s、线程 3s」完全正常还有连接与服务端排队。看趋势线程应明显快于串行不必死磕精确秒数。7. 小步示例串行 vs 线程池道理讲完该动手了。这一节用最短可运行示例做同一件事的两种写法访问三个「服务端故意延迟约一秒」的地址。先按老习惯一个接一个请求再改用线程池一起请求用墙上挂钟perf_counter比比谁更快。你会第一次在自己屏幕上看到状态码可以都是成功但总耗时差一截——这就是「等待重叠」的证据。顺带认识 map、submit、max_workers 这些以后写作业真正会敲到的词。若结果和别人秒数不同回想上一节的时间账看趋势即可。三个「服务端延迟约 1 秒」的 HTTP 请求。importconcurrent.futuresimporttimeimporturllib.request URLS[https://httpbingo.org/delay/1]*3deffetch(url:str)-int:requrllib.request.Request(url,headers{User-Agent:python-lab-day35/1.0})withurllib.request.urlopen(req,timeout40)asresp:resp.read()returnresp.status t0time.perf_counter()print([fetch(u)foruinURLS],fserial{time.perf_counter()-t0:.2f}s)t1time.perf_counter()withconcurrent.futures.ThreadPoolExecutor(max_workers3)asex:print(list(ex.map(fetch,URLS)),fthreads{time.perf_counter()-t1:.2f}s)预期形态两边状态码都是[200, 200, 200]线程总秒数明显小于串行。计时的是墙钟时间返回的列表是新对象不改远端数据。7.1mapvssubmit了解线程池接活文档里常跳出 map 和 submit 两个词初学容易慌。其实可以记成人话map 像「按名单批量派活收回来的结果顺序还和名单一致」submit 像「一单一下达谁先干完谁先交差」更灵活但默认不保证顺序。大多数入门作业、以及本课综合实验用 map 就够了。submit 和 as_completed 更适合以后做进度条、先完成先展示的场景。这一小节只要求你分得清两者侧重点不必两个都写得很溜。API特点ex.map(fn, iterable)保序返回结果写法像mapex.submit(fn, *args)得到Future可先丢任务再取结果入门用map足够。7.2max_workers怎么想max_workers 这个参数名字长意思却很土最多同时雇几个跑腿的。雇 1 个基本还是串行雇的人数和任务数差不多等待才叠得起来雇到上百个调度、建连、对方限流都可能让你更慢甚至把自己机器拖卡。所以「线程越多越快」是错觉。教学实验里三个延迟地址配三个 workers是为了让「同时等三壶水」的图像最直观。以后上生产要按接口承受力和本机资源调而不是拍脑袋写 1000。workers直觉1近似串行等于任务数任务都能同时等 IO任务不多时过大调度与连接压力上升未必更快教学实验用3对应 3 个 URL 即可。8. 综合实践前面的小步代码适合理解这一节把它收成一份「从零到跑通」的完整实践建目录、一键生成脚本、运行、对照输出说明。目标很明确——让你在自己电脑上留下一份串行对比线程的证据而不是只听别人说「并发会更快」。请尽量真的跑一遍。跑的过程中把 serial 和 threads 的秒数记下来后面做题和复习时会用到。若网络不好导致都偏慢只要线程明显短于串行实验目的就达到了。8.0 环境动手前先检查环境能少踩一半坑。你需要不太旧的 Python建议 3.10 以上、能访问公网演示站 httpbingo.org以及能舒服地敲 bash 命令的环境。Windows 同学继续推荐 Cygwin 或 WSL这样后面的 cat 一键生成脚本才能和文中一致。如果公司网络拦了外网演示站实验会失败那是网络策略问题不是并发概念错了——可换网络或换可访问的 delay 类接口再试。项目要求Python3.10网络能访问httpbingo.orgWindows推荐 Cygwin 或 WSLmkdir-p~/python-lab/src/day35cd~/python-lab/src/day358.1 一键脚本下面是一键脚本整段复制到终端会生成 concurrency_demo.py 并执行。脚本结构很直白——先串行请求三次延迟接口再线程池请求三次打印状态码和秒数最后给一句选型提醒。你不需要先手打每一行逻辑先跑通建立感性认识再回头对照源码里的 run_serial 和 run_threads会更容易看懂 ThreadPoolExecutor 是怎么接上 fetch 的。catconcurrency_demo.pyEOF # Day35: concurrency model comparison (IO-bound) import concurrent.futures import time import urllib.request URLS [ https://httpbingo.org/delay/1, https://httpbingo.org/delay/1, https://httpbingo.org/delay/1, ] def fetch(url: str) - int: req urllib.request.Request(url, headers{User-Agent: python-lab-day35/1.0}) with urllib.request.urlopen(req, timeout40) as resp: resp.read() return resp.status def run_serial(): t0 time.perf_counter() codes [fetch(u) for u in URLS] return time.perf_counter() - t0, codes def run_threads(): t0 time.perf_counter() with concurrent.futures.ThreadPoolExecutor(max_workers3) as ex: codes list(ex.map(fetch, URLS)) return time.perf_counter() - t0, codes def main(): print(--- serial ---) sec, codes run_serial() print(statuses:, codes) print(fseconds: {sec:.2f}) print(--- threads ---) sec2, codes2 run_threads() print(statuses:, codes2) print(fseconds: {sec2:.2f}) print(--- note ---) print(CPU-bound work prefers processes; pure waiting on network likes threads/async.) if __name__ __main__: main() EOFpython3 concurrency_demo.py8.2 实测输出秒数随网络变化看趋势这里贴一份实验环境的参考输出。请务必记住你自己的 seconds 几乎肯定和文中不同公网波动太正常了。判读时只抓两件事状态码是不是都是 200threads 的秒数是不是明显小于 serial。如果两边都成功但线程没有更快先检查是不是 workers 过小、网络极差、或演示站限流也可以多跑两次看趋势。输出表会帮你逐项解读每一行在说什么。--- serial --- statuses: [200, 200, 200] seconds: 9.46 --- threads --- statuses: [200, 200, 200] seconds: 3.20 --- note --- CPU-bound work prefers processes; pure waiting on network likes threads/async.对照阅读输出含义两边都是 200请求都成功serial 秒数大等待在相加threads 秒数小等待在重叠note 那行选型口诀提醒协程路线第 36 课用asynciohttpx专攻。9. 和「多开几个脚本」的区别学到这里有人会抬杠我不写线程池开三个终端窗口每个窗口跑一个脚本不也是同时跑吗能那确实也是一种「并发」但很粗糙——结果难汇总到一个程序里超时、日志、失败重试也很难统一管理。课程强调进程内的线程池是因为你要的是「可编程的编排」一个入口、一批任务、一套错误处理、一份结果列表。这一节把三种做法并排比较帮你理解为什么工程里更认线程池/进程池/任务队列而不是靠人手点窗口。做法问题手动开 3 个终端各跑脚本难汇总结果、难统一超时与日志程序内线程池一个进程内编排结果好收集系统级多进程任务队列更重属于进阶部署本课先掌握进程内线程池即可。10. 常见问答前面概念、实验都过了一遍真正卡住学员的往往是几句反复出现的话为什么我这边线程没有更快为什么不直接学协程跳过线程秒数每次都不一样是不是错了任务抛异常会怎样这一节用问答形式集中回答。它们不是题外话而是把「选型、生态、测量、错误」这些实战细节补齐。读完你应更有底什么时候线程该快、什么时候不该指望它快以及下一课协程会补哪一块拼图。Q线程是不是一定比串行快AIO 等待明显时通常更快任务极短或全是 CPU 时不一定。Q为什么不直接学协程跳过线程A大量现有库是同步的线程池是接requests的最短路径。协程下节补。Qseconds每次不一样正常吗A正常。公网抖动、冷启动、限流都会影响。Q出异常会怎样Amap时某个任务抛错迭代结果时可能看到异常。生产要做错误隔离进阶。11. 落地场景故事名词记不住时故事往往记得住。这一节把并发选型放进三个很常见的后台故事要同步很多个部门的接口数据、每晚压缩一大目录日志、页面启动时只拉一次配置。你会看到前两个慢的原因根本不同药方也不同第三个甚至不该上并发。学完试着用自己的话复述这三个故事——能复述说明选型直觉已经开始长在你脑子里而不只在表格里。场景串行痛点并发改法后台同步 20 个部门的接口数据20 倍等待线程池max_workers5每晚压缩日志目录CPU 打满单核进程池按文件并行页面一次只拉 1 个配置无痛点保持串行12. 迷你对照submit 写法了解如果你已经会用 map可以再多看一眼 submit 写法。它解决的问题是任务完成有先有后我希望谁先做完就先处理谁比如刷新进度条、先展示先返回的结果。这不是本课考试必背却是从「作业代码」走向「更像产品代码」的一小步。读的时候对比 map 的保序理解 as_completed 的「不保序但更灵活」即可有印象以后用得到。fromconcurrent.futuresimportThreadPoolExecutor,as_completedimporturllib.requestdeffetch(url:str)-tuple[str,int]:requrllib.request.Request(url,headers{User-Agent:python-lab-day35/1.0})withurllib.request.urlopen(req,timeout40)asresp:resp.read()returnurl,resp.status urls[https://httpbingo.org/get?n1,https://httpbingo.org/get?n2]withThreadPoolExecutor(max_workers2)asex:futures[ex.submit(fetch,u)foruinurls]forfutinas_completed(futures):print(fut.result())和 map 的差别as_completed谁先完成谁先处理不保证顺序map按输入顺序拿结果。需求更合适结果顺序要和输入一致map先完成先处理、做进度条submitas_completed13. 进程池边界示例说明性可不跑重任务本课主实验全是网络等待很容易让人误以为「所有慢都靠线程解决」。这一节补上另一半如果任务是死算进程池大概长什么样Windows 下为什么常见 if name main 保护。代码以说明为主机器忙或时间紧可以先读懂不跑。关键是把边界刻进脑子算力瓶颈时想到进程或专用数值库而不是把 ThreadPoolExecutor 的 workers 调到 64 就完事。fromconcurrent.futuresimportProcessPoolExecutordefcpu_bound(n:int)-int:# 示意纯计算s0foriinrange(n):si*ireturns# Windows 下进程入口保护if__name____main__:withProcessPoolExecutor(max_workers2)asex:print(list(ex.map(cpu_bound,[10_000,10_000])))修改前主线程串行算两遍时间近似相加。修改后两进程多核机器上往往更快。注意函数要定义在可导入位置参数/返回值应可序列化。14. 课堂演示话术给讲师这一小节给自学和授课两种人用。自学时把它当成自问自答的演示脚本先猜串行多久再跑再对比线程再追问「如果改成算一秒质数会怎样」。若你是讲师可以直接按步骤带实验把「等网络」和「死算」两种慢法对比讲透并自然引出下一课协程。五步走完学生通常比只听概念课记得牢。先问学生3 个各等 1 秒的请求串行多久跑 serial读秒数。跑 threads读秒数对比。问若改成「每个任务算 1 秒质数」会怎样引出 GIL/进程。预告协程用更少线程扛更多连接 → Day36。15. 自我检查清单学完一课最容易犯的错是感觉看懂了立刻翻下一课结果两天后什么都剩不下。这一节给你一张很短的自我检查清单请逐条打勾。勾得上的说明选型直觉和最小代码已经到手勾不上的不必硬撑回到对应小节再看五分钟往往比往前赶更有效。清单通过了再进入练习题和下一课协程会轻松很多。能区分并发与并行能判断 IO 密集 / CPU 密集会写ThreadPoolExecutormap知道共享可变状态有风险知道协程里不要硬塞requests实验中线程耗时明显小于串行趋势总结到了收束的时候。如果整课只能装进几句能带走的话就是先看病——慢是因为等还是因为算再开药——线程、进程还是协程用实验亲眼看「网络等待可以重叠」同时记住共享数据会乱、线程不是越多越好。这些句子不追求漂亮追求好用。后面做第 36 课协程、第 39 课流水线时你仍会反复用到今天的分科与选型。先分IO / CPU再选线程、进程或协程。线程池适合重叠网络等待进程偏重算协程适合高并发 async IO。共享可变状态是坑计时看墙钟workers 不是越大越好。实验结论同等 3 个 delay 请求线程总耗时应明显低于串行。小练笔练习题用来自测不要求一次全对更不要先翻答案。题型覆盖选择、判断、简答还有可选实践建议你真的改 URL 数量或 max_workers 跑一跑把秒数记在笔记本上——亲手记过的数比看教材里的 9.46 更有感觉。做题时优先用自己的话解释「为什么」能说清原因比选对字母更重要。卡很久的题回到 IO/CPU 分科和共享数据两节通常就能想通。题 1压缩 1000 张大图CPU 重优先A. 多线程B. 多进程C. 只开协程硬算题 2判断CPython 上多线程一定让 CPU 密集任务接近 8 倍速8 核。题 3本课实验里为什么线程总时间往往明显小于串行题 4并发和并行差一句话题 5在 asyncio 里直接requests.get可能有什么问题题 6下列哪项更适合继续用串行A. 同时下载 50 个文件B. 调一次「查余额」接口C. 压缩 200 个视频题 7ThreadPoolExecutor.map相对手写多线程主要好处是什么题 8判断线程数设为 1000 一定比 10 更快。题 9可选实践把URLS改成 5 个 delay 地址比较max_workers2与max_workers5的耗时趋势记下你的秒数。题 10两线程同时对全局total 1执行 10 万次为什么结果可能不是 20 万小练笔参考答案参考答案在下面。请先自己做完再看否则练习会变成「阅读理解抄写」。答案只给标准方向简答题意思对就行不必和原文一字不差。若你的表述和答案不同但道理成立完全可以算对。真正要修正的是那些和 IO/CPU 分科、GIL 直觉、共享数据结论明显冲突的理解。题 1B题 2错题 3网络等待可重叠不必一个完全结束再开下一个。题 4并发是任务交替推进并行是多核真同时执行。合理即可题 5阻塞事件循环协程失去「等待时切换」的优势。题 6B题 7统一管理线程数量、更易收集结果、代码更短。合理即可题 8错题 9以你机器输出为准一般 workers 从 2 增到 5总耗时会下降但受服务端与网络限制。题 10不是原子操作中间状态交错会导致丢失更新。