await 并非上下文切换:解析 Python 协程与任务的本质差异
Python 的异步模型常被误解,尤其来自 JS 或 C# 背景的工程师。在 Python 中,等待协程不会让出事件循环。唯有任务才能创造并发。本文阐释这种区别的重要性,及其对锁机制、设计和正确性的影响。
Python 的异步模型常被误解,尤其来自 JS 或 C# 背景的工程师。在 Python 中,等待协程不会让出事件循环。唯有任务才能创造并发。本文阐释这种区别的重要性,及其对锁机制、设计和正确性的影响。
每位工程师都曾在代码审查时,被某条评论困扰得比预期更久。
我的经历源于一条简单建议:
“此处需增加锁:该代码为异步执行,可能发生任何交错操作。”
问题代码涉及共享缓存,表面看这条评论合情合理。多个 asyncio 任务同时访问同一结构,而修改它的函数是异步的。这难道不意味着需要更多锁吗?
这次代码审查让我陷入了思考的深渊。问题不在于缓存(它很小),而在于许多工程师(包括资深者)对 Python 异步系统的认知模型。这种模型深受 JavaScript 或 C# 的影响:在这些语言中,await 意味着“立即将控制权交还给运行时”。
但Python并非这些语言。误解这种根本差异会导致不必要的锁定、意外复杂性和隐蔽缺陷。
本文正是我希望更多工程师能理解的解释。
误区:await会放弃控制权(在所有语言中都如此…对吧?)
若你来自JavaScript背景,规则很简单:
- 每次await都会立即让出控制权给事件循环。
- 每个异步函数始终返回任务(Promise)。
- 写下 await 的瞬间,运行时即可调度其他任务。
在 C# 中,逻辑几乎完全一致:
async函数返回Task<T>或Task。await始终代表暂停点。- 运行时决定何时恢复执行。
在Java的虚拟线程世界(Project Loom)中,原理高度相似:当你提交异步任务(通常通过基于虚拟线程的ExecutorService)时,实际上是在创建任务。而调用Future.get()时,虚拟线程会暂停直至结果就绪。这种挂起成本低廉,但仍构成完整的调度边界。
因此开发者需牢记一条核心规则:
“任何异步边界都是挂起点。”
随后他们将这条规则带入Python。
但Python有所不同:它包含两种异步概念
Python将异步划分为:
1. 协程
通过async def定义,但不参与调度。协程对象本质是带潜在中断点的状态机。
async def foo():
return 42
coro = foo() # <- coroutine object, not running
当执行:
result = await foo()
Python会立即进入协程,在当前任务中同步执行,直至完成或遇到中断点(等待未就绪的对象)。
此处不涉及事件循环调度。
2. 任务
通过 asyncio.create_task(coro) 创建。任务是 Python 中并发的基本单元。事件循环交错处理的是任务,而非协程。
此区别绝非表面现象:正是这个原因导致许多开发者误解 Python 的异步语义。
核心真相:对协程的 await 不会将控制权交还事件循环
本句概括了整篇文章的核心:
等待协程不会将控制权交还给事件循环。等待任务才会。
协程更像可暂停的嵌套函数调用,但默认不会暂停。只有当遇到未就绪的可等待对象时才会让出控制权。
对比:
- JavaScript
- Java
- C#
这些语言不会暴露此差异。在它们中,“异步函数”始终是任务。你永远不会等待“裸协程”。每次await都可能引发上下文切换。
Python打破了这种假设。
具体示例1:等待协程是同步操作
让我们将行为明确化:
import asyncio
async def child():
print("child start")
await asyncio.sleep(0)
print("child end")
async def parent():
print("parent before")
await child() # <-- awaiting a coroutine (not a task)
print("parent after")
asyncio.run(parent())
输出:
parent before
child start
child end
parent after
注意以下未发生的情况:
- 在“child start”与“child end”之间未执行其他任务。
await child()阻止事件循环调度其他任务,直至child()自身等待asyncio.sleep。
await child() 直接将协程主体内联执行。
这与 JavaScript 的行为不同。这与 C# 的行为不同。这与 Java 的行为不同。
具体示例 2:任务真正引入并发
修改一行代码:
async def parent():
print("parent before")
task = asyncio.create_task(child()) # <-- spawn a task
print("parent after creating task")
await task
此时输出交错顺序取决于调度器:
parent before
parent after creating task
child start
child end
因为现在存在任务,而等待任务会让出事件循环。
并发源于任务,而非协程。
正是这个关键差异导致多数错误的锁定建议产生。
并发由暂停点定义,而非 async 或 await
现在提炼通用规则:
- async def 函数并非自动并发
await除非内部可等待对象暂停,否则不构成调度点- 并发仅存在于任务之间,且仅发生在实际暂停点
这就是为什么我收到的代码审查建议“添加更多锁,这是异步操作!”基于错误的思维模型。
我的变异代码块中不包含任何await操作。唯一的await发生在获取锁之前。因此:
- 关键代码段相对于事件循环是原子性的。
- 没有任何其他任务能在变异操作中插入执行。
- 增加锁点不会提升安全性。
问题不在于缓存机制,而在于评审者的认知偏差。
Python选择此设计的缘由
Python的异步模型源于生成器(yield, yield from),而非绿线程或Promise。协程正是这些基础机制的演进产物。
这种历史传承带来了:
- 结构化控制流与调度并发之间存在更明确的边界。
- 能够编写在实际挂起前表现为同步行为的异步代码。
- 对交错时机实现精细化控制。
这也导致来自 JavaScript、Java 或 C# 的开发者产生困惑——在这些语言中,异步自动意味着“这是个任务”。
Python将“这是否为任务”的判定权交由开发者。
整合认知:真正有效的思维模型
以下是我审阅asyncio代码时倡导的模型:
- 协程是具有潜在中断点的可调用对象:它们不会并发执行。
- 仅任务引入并发性:若从未调用
asyncio.create_task,则可能完全不存在并发。 - 并发仅发生在暂停点:块内无await → 无交错执行 → 无需在此处加锁。
- 锁应保护跨任务的数据,而非协程:在可能暂停处加锁,而非async关键字出现处。
实际代码库的实用指南
- 审查任务创建点:每个
asyncio.create_task()都是并发边界。 - 扫描关键段中的暂停点:若锁内无 await 语句,该代码块相对于事件循环是原子操作。
- 遵循“外部计算,内部修改”原则:先获取锁外计算值,再快速在锁内修改数据。
- 明确区分概念:令人惊讶的是,许多资深工程师仍未真正理解协程与任务的差异。
结论:Python异步与JavaScript异步本质不同
理解以下区别后:
- JavaScript:async function → 始终是任务
- C#:async → 始终是任务
- Java(Loom的
VirtualThread):async → 始终是任务 - Python:async def → 仅是协程;任务创建需显式声明
整个模型便豁然开朗。
Python的await并非上下文切换,而是可能暂停的结构化控制流。
正是这种差异让我未在缓存代码中添加更多锁。这也促使我如今审查Python异步代码时提出更关键的问题:
“这段代码实际可能在何处发生交错执行?”
这个单一问题能发现更多漏洞,消除更多不必要的复杂性,远胜于任何关于异步系统锁定的通用规则。
本文文字及图片出自 await Is Not a Context Switch: Understanding Python's Coroutines vs Tasks
> 等待协程不会将控制权交还给事件循环。
我认为这比初读时想象的更微妙,由于示例选择不当导致概念模糊。
以下是更清晰的说明:
输出结果:
因此作者的观点是:“other”永远不会出现在“parent before”和“child start”之间。
编辑:补充说明
感谢!!帖子里的示例根本没说明问题,快把我逼疯了。
是的,示例写得很草率。
但JavaScript不也如此吗?所以我不太理解作者的观点…是我漏掉了什么,还是作者(或其LLM?)刻意进行了一场与JavaScript无关的比较?
编辑:反复阅读示例后,我99.9%确定这是草率之举并已标记。
编辑2:同作者另一篇文章:https://mergify.com/blog/why-warning-has-no-place-in-modern-…
> 这不仅是文本——它是结构化、可筛选且可执行的。
我的结论是:应该让大型语言模型为我编写浏览器用户脚本,自动标记并隐藏该域名的链接。
> JavaScript不也一样吗?
你说得对,等效的JS脚本会产生相同的输出序列。
原来有办法模拟Python的asyncio.create_task()。
Python:
JavaScript:
> 但JavaScript不也是这样吗?
我不这么认为。虽然我已很久没在两种语言中为棘手的异步问题头疼,但据我所知在JS中会是:
JavaScript 存在微任务和宏任务机制。setTimeout 创建宏任务,而
.then(以及await)创建微任务。微任务会在宏任务之前执行,但仍需等待当前调用栈完成后才触发。
根据原帖(GP的示例更清晰说明)Python的意外之处在于:它只是将被等待的协程放入当前调用栈。因此在Python中
await无法保证任何内容会被放入任务队列(微任务或宏任务)。>我确信在JS中会是[…]
这不合逻辑。这意味着等待函数无法访问Promise的结果(因为它可能在Promise满足前就继续执行),这将破坏Promise的全部意义。
> 微任务在宏任务之前执行
正确。
> 但它们仍需在当前调用栈完成后才被执行。
正确。
> 我确信在 JS 中应该是 […]
你对 JS 事件循环的理解正确,但得出了错误结论。
没错,又是篇垃圾文章。现在我们几乎每天都会收到这类评论,文章本身明显是垃圾内容。
文章一半是段落标题,另一半是项目符号或编号列表。即便提示内容原本有趣,也被大型语言模型彻底抹平——变成毫无视角、毫无表达价值的信息堆砌。我完全无法判断作者可能想传达什么(除了博文点击量和标题之外)。
我真心希望大家能更早识别这类内容。太多人草草浏览后直奔评论区,但我们不该纵容HN沦为低价值文章的温床——仅仅因为它们能引发热议。
我一直在这里标记问题,然后转战kagi平台标注为垃圾内容。真希望我们也能有类似功能,而非仅靠“标记”按钮。
虽然标记时不该评论,但这次情况特殊——我们必须集体提升识别能力,或者开发更有效的工具。
这难道不会让await变成空操作?如果任务不交错执行,异步函数的异步性体现在哪里?
它们在执行'yield'操作时具有异步性,即当函数最终执行I/O操作、睡眠或其他类似操作时。这些才是函数可以交错执行的节点。单纯等待另一个函数绝非此类节点:此处的await仅表示被调用函数可能在执行过程中某个时刻向调度器让出控制权(并非必须如此!),而非调用函数会立即让出控制权。
asyncio.sleep不属于这类函数吗?“parent before”和“parent after”之间理应能插入“other”。
没错,但不能插入在“parent before”和“child start”之间(或“child end”和“parent after”之间)
啊,明白了。这说得通。
任务是通过asyncio.create_task等方法创建的异步函数,它们会自行调度执行。零延迟定时器不会创建任何任务,协程直接在调用者帧内执行,因此本质上是空操作。
这太棒了。
给你讲个恐怖故事。
几年前我在某初创公司担任首席工程师。工程经理总吹嘘自己来自XYZ公司,组织过Pythonista聚会,还夸耀他对Python的博学。当时我们开发安全产品需要快速扫描数十万文档,于是用协程构建了扇出式扫描器。我加入时项目已推进至后期,负责为另一个类似平台添加适配器。目睹所有协程被序列化存储在S3中——以便节点崩溃后能“恢复”运行——却发现竟没有一个create_task方法。所有等待、序列化、恢复尝试、检查、处理、报告、序列化的操作都同步执行。
当我试图指出架构缺陷并与那位自我膨胀先生争执时,我被解雇了。
经典案例。十年前我也曾与某公司CTO发生过类似冲突,他断然拒绝使用版本控制管理源代码。他坚持让开发者们互相传阅压缩过的目录文件,至于如何整合他人修改?祝你好运吧。
那场争论我赢了,但过程异常艰难。至少事后他还有自知之明承认自己当时很蠢。
这经历可大不相同。
当你自以为通晓一切,便关闭了可能性之门。
自负是工程师最致命的缺陷。
自负确实会阻碍进步,这点与原帖情况相似。原帖作者被解雇了,所幸我幸免于难——但编程环境或任务差异在此问题上远不如相似性重要。
我的新年决心是停止在hacker news抱怨此事,但眼下仍要说:
我认为ChatGPT的行文风格与语气充满居高临下的敷衍感,其平庸程度甚至模糊了原始提示中本应独具匠心、发人深省的洞见。
试图通过反向工程还原“不是这个:是那个!”的句式、人为制造的戏剧性叙事及怪异的强调手法来重现那些洞见和思考,实在令我提不起劲。
或许存在折中方案:HN能否支持添加指向原始创意种子的“提示”链接?
这正是我偏爱Mistral的主因——它始终保持着合理且尊重的语气。
反观ChatGPT,其权威性表述屡屡超越自身能力数个数量级。
赞同。
令人疲惫。
或许有人会开发“文章转提示词”的反向ChatGPT?
当然有人早就做过了,而且它就在ChatGPT内部——我这是怎么想的?不过若真尝试,生成的提示文案读起来实在不怎么舒服:https://chatgpt.com/share/6926f33c-8f98-8011-984e-54e49fdbb0…
你是说这个是ChatGPT生成的?我完全没看出来…你从哪里看出来的?
我认为这个例子完全没用,错误地假设其他语言会给出不同输出,还只顾吹嘘Python模型多么优秀——毕竟它能生成你根本看不懂的微分结果。
希望HN能增加内容过滤机制,别再浪费时间在这种垃圾帖上了
诸如“常见误解”“核心真相”“为何选择”‘综合分析’“注意未发生的情况”这类表述。不仅如此,整篇帖子的措辞都令人不适。
引用ChatGPT自身的话:
该文章遵循“经典”结构:提出常见误解→阐明问题所在→展示具体案例→给出明确结论。段落布局均衡,每个部分都传递着精准的逻辑步骤。这种结构虽利于清晰表达,却也容易让人感觉像是许多AI生成或营销类博文惯用的“标准模板”。
若能同时成为一篇文笔精湛的博文就更好了。但我觉得AI天生就趋向这种基础结构,而人类撰写的博文通常会忽略一两项“写作规范”,反而让文章更具可读性(毕竟并非所有内容都需要严格遵循结构才能引人入胜)。
有意思。我可能只是缺乏这方面的经验。你这么一说我确实更明白了,不过不确定AI在其中占比究竟有多大…
> 整合所有要素:真正有效的思维模型
暴露无遗
我同意那部分确实很像ChatGPT的风格(之前没注意到),但仅凭一行AI生成的内容,我无法断定整段文本主要由AI创作…你觉得呢?完全可能原稿只是被AI在细节处稍作润色。
你在讽刺吗?若非如此,这种套路看多了自然一眼就能识破。
不,我是认真的。或许我接触得还不够多,尚无法分辨。
这种文风专为营销话术优化。它极具吸引力,尤其对两类人群:1. 不知这是垃圾代码的人 2. 不求深度知识只图娱乐的读者。
同理,编程也专为教程级代码优化——忽略异常处理,留下“正式环境请执行XYZ”的注释等等…
这就是我所说的居高临下。我不需要被推销“代码评审时炫技的小技巧”,我真正想学的是Python异步编程!
或许是我理解有误,但示例似乎缺乏说服力?无论哪种语言,打印顺序都显而易见:示例1中我们明确等待子任务完成,示例2中两个父任务的打印语句都位于await之前。因此我认为这两个示例都未能阐明作者试图传达的核心观点?
我同意。若严格遵循JavaScript中“示例1”的语法(在同一行调用并等待),其可观察输出结果与Python完全一致。
我推测作者想说明的是:若先调用异步函数,随后再执行
await,行为会有所不同。任何具备异步特性的语言都应产生完全相同的结果。其他评论已说明示例应如何呈现及其差异。
简而言之:当其他协程运行并等待sleep()时,可能出现“父进程先完成”到“子进程启动”的各种情况。Python中不可能发生这种情况,因为子进程不会作为新任务运行。
若理解有误还请指正:第一个示例在JavaScript中是否也会产生相同行为?由于parent()函数“等待”child()完成,因此无法实现print语句的交错执行。
这个StackOverflow问题的示例可能更具说明性:https://stackoverflow.com/q/63455683
整篇文章有些混乱。比如这段:
> 我的突变块中没有 await。唯一的 await 发生在获取锁之前。因此:
> * 关键区块相对于事件循环是原子性的。
> * 其他任务无法在变异操作中插入执行
> * 增加锁数量不会提升安全性
这与JavaScript的逻辑完全一致。虽然Python异步实现与其他语言存在诸多细微差异,但本文既未通过框架故事阐释,也未通过示例说明任何实质内容。
若将其与JS世界对比,Python的async机制似乎更接近Babel风格的生成器协程[1],而非JavaScript的async/await执行模型。
[1] https://babeljs.io/docs/babel-plugin-transform-async-to-gene…
你的理解没错,文章中的第一个示例在JS中行为完全一致,详见https://jsfiddle.net/L5w2q1p7/。
我认为在JS中更容易理解,因为Promises与async/await之间存在对应关系。
因此将你的示例进行语法糖化后,行为会更明显:
也许我有点吹毛求疵,但第二个具体示例中父方法的流程与第一个不同。如果第二个示例像下面这样修改第一个示例,那么两种情况的输出结果将相同。
个人而言,我始终无法让Python的异步机制正常工作。而在Node.js中,只需将前缀数组映射为ListBucket Promise数组,就能并行调度足够多的S3 ListBucket网络请求,从而占满CPU核心。接着用Promise.all()让它们同时执行。
Python里asyncio和threading各有千秋,但感觉入门门槛太高,根本没法快速上手。难道大家都是需要时才临时抱佛脚?真有人觉得这玩意儿有趣吗?
你应该映射到 asyncio.create_task,然后用 asyncio.gather 收集结果来填满你的 CPU 核心。
trio 库有一个出色的教程,详细解释了所有这些概念[0],即使你不使用 trio 而坚持使用核心 Python 库,也值得一读:
https://trio.readthedocs.io/en/stable/tutorial.html
本文对Java虚拟线程的理解存在误区。虚拟线程采用抢占式并发模型而非协作式,因此挂起点不仅存在于Future.get()等调用处
我有些困惑——当然,若立即执行await,它在返回前就不可能执行其他操作。
但如下代码运行结果符合预期:
真正的区别在于协程在被等待前不会执行任何操作,但我认为asyncio任务在实质上并无显著差异。它本质上只是一个封装器,通过实际任务管理器实现“并发”执行。
Python确实存在两种协程:生成器和异步函数。它们可以相互转换,
多年前我在Stack Overflow上问过完全相同的问题,当时得到过一个不错的解答:https://stackoverflow.com/questions/57966935/asyncio-task-vs…
异步和并行编程是我从未真正学过、甚至不敢使用的概念,因为我始终无法理解代码的运作机制,甚至不清楚这类代码的正确编写方式。有人能推荐帮助你掌握这些概念并建立良好思维模型的优质学习资源吗?
光看书永远学不会,必须亲身体验。动手实践才是关键。
编写一个简单的单线程HTTP服务器,接收字符串并用低效算法(如高成本的bcrypt)进行哈希处理(或直接在返回前加睡眠)。
编写集成测试(无需复杂),用10、100、1000个请求反复轰炸服务器。
记录请求批次处理的性能表现(或低效表现)。
现在尝试编写多线程服务器,让每个请求启动新线程。
性能如何?(提示:了解全局解释器锁GIL)
嗯,可能创建了过多线程?学习线程池及其在资源约束方面的优势。
性能提升了吗?尝试改用multiprocessing.Pool来突破GIL限制。
想尝试异步?操作相同!但异步的核心在于让单线程高效运行无空闲时间,而bcrypt这类算法会占用大量CPU资源。此时应将bcrypt替换为await asyncio.sleep()来模拟低速网络请求。若要在异步函数中使用bcrypt,务必将其任务委托给multiprocessing.Pool。接下来请尝试此方案。
学习可以如此简单。阅读Thread、multiprocessing和asyncio的文档吧。Python文档通常简明扼要,更重要的是它们比某些随意的个人博客更准确可靠。
我最大的收获是:在Python中以下写法不可行:
这种行为违背了其他语言的直觉,似乎也削弱了async/await的核心优势(轻松编写异步操作)?
我见过太多脚本,本该并发的任务却因作者懒得处理异步所需的冗余代码而无法实现。JavaScript风格的async/await完美解决了这个问题。
我认为这更贴近作者想阐明的核心思想。
两个示例在C#中实现效果相同,且都说明协程会同步执行直至遇到真实暂停点。将
asyncio.create_task替换为Task.Run后,第二个示例的行为也完全一致。致JS开发者:借助出色的Effection库,可在JS中实现类似的实用行为(甚至更多功能)。
https://effection-www.deno.dev/
我倾向于在任何场景都使用生成器,因为它能更精细地控制执行流程。但同时代码会“感染”意外类型。实际上这与async/await和Promise原理相似,只是后者已在生态系统中广泛应用。
讽刺的是,作者试图通过类比其他语言解释Python的awaitable对象时,反而暴露了他对其他语言异步模型的理解有多么肤浅哈哈
对任务调度稍有兴趣者,强烈推荐Phil-Op的内核构建教程——其GitHub分支专门聚焦异步任务。本周我实现多核功能时,终于深刻领悟了底层机制的本质。这得益于Claude/Gemini的辅助,省去了熬夜研究内核调试方法和文档的麻烦;堪称我接触异步编程的最佳体验
这是否等同于此处讨论的两种模型差异:异步函数“在首次await前同步执行”与“执行前总会先yield一次”?
https://www.reddit.com/r/rust/comments/8aaywk/async_await_in…
我 认为 这里至少涉及两个自由度,但 相当 不确定,如有误请指正:
– 当调用 MyCoroutine()(作为普通函数调用——无 await 等操作)时:其主体是否立即开始执行?还是仅返回某种可等待对象以便稍后启动?
– 当等待该调用结果时(使用语言的await运算符及启动执行的额外函数调用):是否会在某个节点强制交出调度器控制权,还是直接开始执行主体?
文章似乎将两者视为不可分割的整体,仅讨论整体行为的前后阶段,但您提供的链接似乎只讨论了第一点?
再次强调,我对此相当不确定,想听听大家的看法。
有多少人会不惜一切代价避免在Python中使用await(例如更倾向于使用multiprocessing之类的方法)?在Python中使用await总让我感觉不如其他语言顺手。
需要说明的是,Rust中使用tokio::spawn(及其替代方案)时行为相同。
asyncio的常见用法是像fastapi这样的服务器框架来调度任务。我曾使用这类框架一段时间,后来才意识到需要创建_task来实现任务内部并发。
我确信这篇帖子有误。JS在此示例中的行为与Python完全一致。C#的情况不确定,但推测应该没有区别。
那个用parent()等待child()的示例…根本没效果?或许我漏掉了某些细节,但这里有一个等效的JS示例:
输出结果如下:
与Python版本完全一致。本文混淆了JavaScript中返回Promise的代码远多于Python的事实,误以为这导致行为差异。事实并非如此。
在Python中,无需asyncio库即可通过累积协程并按任意顺序等待来实现自定义事件循环。但Python本身并不提供内置事件循环。JavaScript同样支持此操作,但其内置事件循环相当复杂(参见微任务机制)——没有该机制就无法运行环境,若需额外事件循环则必须自行实现。
create_task() 仅向事件循环注册一个协程,并返回一个“未来对象”,其本质是宣告:“当主事件循环完成对该协程的等待后,此对象即为获取结果/异常的凭证”。这正是事件循环的精妙之处。其差异如同:将衬衫送洗后守候在店里(虽未实际劳动,但也无法处理其他事务),与送洗后离店用餐,返店时手持取件单等待(并发处理)。
但本质上,在单线程环境中等待一个实际不执行异步操作的异步函数,并不会带来并行性。更多内容详见第11节。
我虽未深入研究Python异步编程(多年前接触过gevent),但必须承认自己曾存在文中描述的误解… 受益匪浅!
但如何系统性地审计大型异步代码库,找出所有隐藏的交错点和冗余锁?
这与Kotlin的suspend和协程相比如何?(我最熟悉的是后者)
当然这篇文章观点很有道理,但它几乎像念咒语般反复强调:代码里没有asyncio.create_task就不是并发。至少应该提一下asyncio.gather吧,我认为用它来解释示例会更合适
你说的没错,但asyncio.gather的开头几行会自动用create_task封装传入的参数——前提是这些参数本身不是Task对象。
我觉得这篇文章有点偏离重点。
它说的是调用异步函数(比如你自己写的函数)的行为本身不是yield点。真正的yield点只有在调用会阻塞等待外部事件(如IO或时间)的地方——
await asyncio.sleep(100)就是其中之一。此论断虽正确,但显然无关紧要?任何异步函数调用在其可能的调用树中都存在此类yield点。 若不存在此类点,该函数根本无需标记为异步 。
恕我直言,本文存在缺陷/不完整。
说实话…我并不乐于指出这些问题,因为我曾 完全处于作者撰写时的境地 :面对大量看似异步实则毫无并发性的代码,面对大批工程师坚信“只要代码里有async/await,性能就会自动提升,就像Golang那样”,以及那些复杂且漏洞百出的异步控制流——这些控制流最终都只是将底层同步阻塞操作封装在线程池里罢了。但本文仍存在多处错误与疏漏。首先,它将异步控制流等同于“异步”,这种观点完全错误。Golang“般自动高效”,以及那些复杂又漏洞百出的异步控制流——最终都只是在线程池底层包裹着阻塞的同步操作。
但本文仍存在多处错误与疏漏。
首先,它混淆了任务创建与延迟任务启动的概念。这两种行为毫无关联。调用“await asyncfunc()”会启动asyncfunc()中的生成器;调用“await create_task(asyncfunc())”同样如此。而直接调用“create_task(asyncfunc())”(不加await)则会将asyncfunc()加入任务队列,待控制权返回事件循环时,由 事件循环 启动其生成器。
其次,正如其他评论者指出的,它对竞争性并发系统(Loom/C#/JS)的描述存在偏差。
第三,其“必须调用create_task()才能实现并发”的宣传语并不完整——标准库中某些常用组件会自动为你调用create_task(),例如asyncio.gather()等。请在https://docs.python.org/3/library/asyncio-task.html中搜索“automatically scheduled as a Task”
第四点——这看似是吹毛求疵的边缘案例,但我见过大量不知情的代码依赖于此——“协程中的await不会暂停事件循环”仅在 通常情况下 成立。存在少数特殊非任务型awaitable对象会 确实 回退至循环(相当于JavaScript的process.nextTick)。
以下代码可说明此现象:
按此实现,代码将支持文章首节所述:程序将在while-True-await-noop()中无限忙等待,永远不会打印“Sleep loop”。
关联我上述第一点:若将“await noop()”替换为“await create_task(noop())”,代码仍会形成忙循环,但每次循环迭代都会触发yield/nextTick效果,因此会输出“Sleep loop”。目前效果良好。
但如果将“await noop()”替换为“await asyncio.sleep(0)”呢?asyncio.sleep具有特殊性:它本质上是普通的纯Python“async def”函数,但会调用两类异步内置行为(对于sleep-0情况,其任务协程主体仅为“yield”;对于sleep-非零情况,则为asyncio.Future)。即使忙等待是针对 sleep-0 且未触发任何任务/未来对象,它仍会立即让出控制权。这种特殊行为导致文章代码中多个示例产生混淆,因为“await returns-right-away”与“await asyncio.sleep(0)”的行为 并不等价 。
同样地,若将“await noop()”替换为“await asyncio.futures.Future()”,任务就会执行。这揭示了Python asyncio的 真正 核心原则(必须承认,本文对此的阐述已相当接近!):
如果这段代码
await asyncio.sleep(0)
都不能将控制权交还给事件循环,那它到底有什么用?
它确实会交出控制权。据我所知,这才是“正确用法”。但示例不够理想,因为此时没有其他任务可切换,事件循环会直接回到中断处继续执行。尽管文章描述不同,我确信在JS、C#和Java中也会出现相同情况。
明白了,谢谢说明
文章写得不错,但示例确实不够理想