发布于 2026-01-06 2 阅读
0

并发汉堡 - 理解 async/await

并发汉堡 - 理解 async/await

本文位于:

引言

现代版本的 Python(以及其他语言)支持使用称为“协程”的“异步代码”,并具有相应的语法。asyncawait

这里提供一个友好且不太技术性的解释,以帮助大家对异步代码、并发和并行等概念建立一些直观的理解。

这段文字摘自FastAPI 的文档,FastAPI 是一个用于在 Python 中构建 API 的现代框架。

虽然本文是为 Python 和 FastAPI 编写的,但其中的所有故事和信息也适用于其他具有async和 的语言await,例如JavaScriptRust


现在,让我们在下面的章节中逐部分地分析这个短语:

  • 异步代码
  • asyncawait
  • 协程

异步代码

异步代码仅仅意味着编程语言💬有一种方式告诉计算机/程序🤖,在代码的某个地方,它🤖必须等待其他地方的某个操作完成。假设这个“其他地方”叫做“慢文件”📝。

所以,在那段时间里,电脑可以去做其他工作,而“慢文件”📝则可以完成。

然后,计算机/程序会在每次有机会的时候再次返回,因为它可能正在等待,或者在它完成当时的所有工作之后。它会检查它等待的任务是否已经完成,并执行它必须执行的操作。

接下来,它会🤖完成第一个任务(比如说我们的“慢文件”📝),然后继续处理与该任务相关的任何事情。

这种“等待其他操作”通常指的是相对较慢的I/O操作(与处理器和 RAM 内存的速度相比),例如等待:

  • 客户端要通过网络发送的数据
  • 您的程序通过网络发送给客户端的数据
  • 磁盘上某个文件的内容将被系统读取并传递给你的程序。
  • 你的程序传递给系统要写入磁盘的内容
  • 远程 API 操作
  • 完成数据库操作
  • 数据库查询以返回结果
  • ETC。

由于执行时间大部分消耗在等待I/O操作上,因此它们被称为“I/O 密集型”操作。

之所以称之为“异步”,是因为计算机/程序不必与慢速任务“同步”,不必等待任务完成的确切时刻,也不必什么都不做,就能获取任务结果并继续工作。

相反,由于它是一个“异步”系统,任务完成后,可以稍等片刻(几微秒),等待计算机/程序完成它要做的事情,然后再回来获取结果并继续处理它们。

对于“同步”(与“异步”相对),他们通常也使用“顺序”一词,因为计算机/程序会按顺序执行所有步骤,然后再切换到不同的任务,即使这些步骤涉及等待。

并发和汉堡

上述异步代码的概念有时也称为“并发” 。它与“并行”不同

并发并行都与“不同的事情或多或少同时发生”有关。

但并发并行之间的细节却截然不同。

为了更好地理解其中的区别,请想象一下下面这个关于汉堡的故事:

同时汉堡

你和你的暗恋对象😍一起去吃快餐🍔,你排队等着收银员💁接受前面人的订单。

接下来轮到你了,你点两份非常精致的汉堡🍔,一份给你心仪的对象😍,一份给自己。

你付钱💸。

收银员💁跟厨房里的伙计👨‍🍳说了些什么,让他知道他必须准备你的汉堡🍔(尽管他目前正在准备上一批客人的汉堡)。

收银员会告诉你轮到你的号码。

在等待的时候,你和你的心仪对象一起😍,选了一张桌子,坐下来和你的心仪对象😍聊了很久(因为你的汉堡很精致,需要一些时间准备✨🍔✨)。

当你和你的暗恋对象坐在桌旁等待汉堡的时候😍,你可以利用这段时间欣赏你的暗恋对象是多么优秀、可爱和聪明✨😍✨。

在等待和与心仪对象聊天时😍,你会不时查看柜台上显示的号码,看看是否已经轮到你了。

然后,终于轮到你了。你走到柜台,拿了汉堡🍔,然后回到桌边。

你和你的暗恋对象😍一起吃汉堡🍔,度过了美好的时光✨。


想象一下,你是故事中的电脑/程序🤖。

排队的时候,你只能闲着😴,等着轮到自己,没做什么“有意义的”事情。不过队伍移动很快,因为收银员💁只负责接单(不负责准备食物),所以没关系。

然后,轮到你的时候,你就可以做真正的“生产性”工作了🤓,浏览菜单,决定你想吃什么,拿到你心仪对象😍点的菜,付款💸,检查你给的账单或卡是否正确,检查收费是否正确,检查订单上的菜品是否正确等等。

但是,即使你的汉堡🍔还没做好,你和收银员💁的交流也“暂停”⏸,因为你必须等待🕙你的汉堡做好。

但是,当你离开柜台,坐在桌边等待轮到你时,你就可以把注意力转移到你喜欢的人身上,并“努力”和他/她调情。这样,你又在做一些非常“有意义”的事情了,就像和你喜欢的人调情一样。

然后收银员💁会说“汉堡做完了”🍔,并把你的号码写在柜台显示屏上。但当显示屏上的号码变成你的号码时,你不会立刻兴奋地跳起来。你知道没人会偷你的汉堡🍔,因为你知道自己的号码,他们也知道自己的号码。

所以你等着你的暗恋对象😍讲完故事(完成当前工作⏯/正在处理的任务🤓),温柔地微笑着说你要去吃汉堡⏸。

然后你走到柜台🔀,完成之前的任务⏯,挑选汉堡🍔,道谢,然后把汉堡带到桌子上。这样就完成了与柜台互动这一步骤/任务⏹。这反过来又创建了一个新的任务——“吃汉堡”🔀⏯,但之前的“取汉堡”任务已经完成⏹。

平行汉堡

现在让我们假设这些不是“同时进行的汉堡”,而是“并行进行的汉堡”。

你和你的暗恋对象一起去吃快餐🍔😍。

你排队等候,而几位(比如说 8 位)收银员同时也是厨师👨‍🍳👨‍🍳👨‍🍳👨‍🍳👨‍🍳👨‍🍳👨‍🍳👨‍🍳会接受你前面顾客的订单。

你前面的每个人都在等待他们的汉堡做好后再离开柜台,因为8位收银员都是亲自去准备汉堡,然后马上处理下一份订单。

终于轮到你了,你点了两份非常精致的汉堡🍔,一份给你心仪的对象😍,一份给自己。

你付钱💸。

收银员去了厨房👨‍🍳。

你站在柜台前等待🕙,以免别人在你之前拿走你的汉堡🍔,因为没有排队号码。

当你和你的暗恋对象😍忙着不让任何人插队抢走你们的汉堡🕙时,你根本无暇顾及你的暗恋对象😞。

这是“同步”工作,你需要与收银员/厨师👨‍🍳“同步”。你必须等待🕙,并在收银员/厨师👨‍🍳做好汉堡🍔并交给你的那一刻准时到达,否则,其他人可能会拿走它们。

然后,在柜台前等了很久之后,你的收银员/厨师👨‍🍳终于带着你的汉堡🍔回来了🕙。

你拿着汉堡🍔和你的心上人😍一起走到桌边。

你只要吃掉它们,就大功告成了🍔⏹。

由于大部分时间都花在了柜台前等待,所以并没有太多交谈或调情。


在这个平行汉堡的场景中,你是一台电脑/程序🤖,有两个处理器(你和你的暗恋对象😍),都在等待🕙,并将注意力⏯投入到“在柜台上等待”🕙中,而且要等很久。

这家快餐店有 8 名员工(收银员/厨师)👨‍🍳👨‍🍳👨‍🍳👨‍🍳👨‍🍳👨‍🍳👨‍🍳👨‍🍳。而同时营业的汉堡店可能只有 2 名员工(一名收银员和一名厨师)💁 👨‍🍳。

但是,最终体验仍然不是最好的😞。


这相当于汉堡包的故事🍔。

举一个更“现实生活”中的例子,想象一下银行。

直到最近,大多数银行都有多个柜员👨‍💼👨‍💼👨‍💼👨‍💼和长长的队伍🕙🕙🕙🕙🕙🕙🕙🕙🕙。

所有收银员都在忙着接待一个又一个顾客👨‍💼⏯。

你必须在队伍里等很久,否则就会错过机会。

你大概不会想带着你的暗恋对象😍一起去银行🏦办事吧。

汉堡结论

在“和心仪对象一起吃快餐汉堡”这种场景中,由于等待时间很长🕙,因此拥有一个并发系统⏸🔀⏯就更有意义了。

大多数网络应用程序都是这种情况。

用户很多,但你的服务器正在等待🕙他们不太好的网络连接来发送请求。

然后再次等待回复。

这种“等待”🕙是以微秒来衡量的,但总的来说,最终还是需要很长时间的等待。

这就是为什么对 Web API 使用异步代码非常有意义的原因。

大多数现有的流行 Python 框架(包括 Flask 和 Django)都是在 Python 引入异步特性之前创建的。因此,它们的部署方式支持并行执行以及一种较旧的异步执行方式,这种方式不如新的异步特性强大。

尽管异步 Web Python (ASGI) 的主要规范是由 Django 开发的,但为了添加对 WebSocket 的支持。

这种异步性使得 NodeJS 流行起来(尽管 NodeJS 不是并行的),而这正是 Go 语言作为编程语言的优势所在。

使用FastAPI也能获得同样的性能水平

由于可以同时实现并行和异步,因此其性能比大多数测试过的 NodeJS 框架更高,并且与 Go 相当,Go 是一种更接近 C 的编译语言(这一切都要感谢 Starlette)

并发性比并行性更好吗?

不!这并不是故事的寓意。

并发与并行不同。在涉及大量等待的特定场景下,并发性能更佳。因此,对于 Web 应用程序开发而言,并发通常比并行更胜一筹。但并非适用于所有情况。

为了平衡这一点,请想象一下以下这个短篇故事:

你必须打扫一栋又大又脏的房子。

是的,这就是全部真相


无需等待🕙,只是有很多工作要做,涉及房屋的多个地方。

你可以像汉堡的例子那样轮流打扫,先打扫客厅,再打扫厨房,但因为你不需要等待任何东西,只是不停地打扫,所以轮流打扫不会产生任何影响。

无论是否轮流(并发),完成任务所需的时间都相同,完成的工作量也相同。

但如果你能找到 8 位前收银员/厨师/现任清洁工👨‍🍳👨‍🍳👨‍🍳👨‍🍳👨‍🍳👨‍🍳👨‍🍳👨‍🍳,让他们每个人(加上你)负责清洁房子的一部分区域,你就可以同时完成所有工作有了额外的帮助,就能更快地完成。

在这种情况下,每个清洁工(包括你)都是一个处理者,各司其职。

由于大部分执行时间都用于实际工作(而不是等待),而计算机中的工作是由CPU完成的,因此他们将这些问题称为“CPU 密集型”问题。


CPU密集型操作的常见例子是需要复杂数学运算的操作。

例如:

  • 音频图像处理
  • 计算机视觉:图像由数百万个像素组成,每个像素有 3 个值/颜色,处理通常需要同时对这些像素进行计算。
  • 机器学习:通常需要大量的“矩阵”和“向量”乘法。想象一下,一个包含大量数字的大型电子表格,需要同时将所有数字相乘。
  • 深度学习:它是机器学习的一个子领域,因此,同样的道理也适用于机器学习。不同之处在于,它处理的不是单个电子表格中的数字乘法,而是庞大的数据集,而且在很多情况下,需要使用专门的处理器来构建和/或使用这些模型。

并发+并行:Web+机器学习

使用FastAPI,您可以利用 Web 开发中非常常见的并发性(这也是 NodeJS 的主要吸引力所在)。

但是,对于像机器学习系统这样的CPU 密集型工作负载,您也可以利用并行性和多处理(多个进程并行运行)的优势。

此外,Python 是数据科学、机器学习,尤其是深度学习的主要语言,这一简单事实使得 FastAPI 非常适合数据科学/机器学习 Web API 和应用程序(以及其他许多应用程序)。

要了解如何在生产环境中实现这种并行性,请参阅FastAPI 文档中的“部署”部分。

asyncawait

现代版本的 Python 提供了一种非常直观的方式来定义异步代码。这使得异步代码看起来就像普通的“顺序”代码一样,并且会在合适的时机自动执行“等待”操作。

当某个操作需要等待一段时间才能给出结果,并且支持这些新的 Python 特性时,你可以这样编写代码:

burgers = await get_burgers(2)
Enter fullscreen mode Exit fullscreen mode

关键在于await。它告诉 Python,在将结果存储到 之前,必须等待 ⏸get_burgers(2)完成其操作 🕙 burgers。这样,Python 就知道在此期间它可以去做其他事情 🔀 ⏯(例如接收另一个请求)。

await使其生效,它必须位于支持这种异步性的函数内部。为此,只需使用以下方式声明它async def

async def get_burgers(number: int):
    # Do some asynchronous stuff to create the burgers
    return burgers
Enter fullscreen mode Exit fullscreen mode

……而不是def

# This is not asynchronous
def get_sequential_burgers(number: int):
    # Do some sequential stuff to create the burgers
    return burgers
Enter fullscreen mode Exit fullscreen mode

有了async def它,Python 就知道,在该函数内部,它必须了解await表达式,并且可以“暂停”⏸该函数的执行,然后去做其他事情🔀,然后再回来。

当你想调用一个async def函数时,必须先“等待”它执行完毕。所以,这样做是行不通的:

# This won't work, because get_burgers was defined with: async def
burgers = get_burgers(2)
Enter fullscreen mode Exit fullscreen mode

其他形式的异步代码

这种用法在语言中相对较新asyncawait

但这让处理异步代码变得容易得多。

这种相同的语法(或几乎相同的语法)最近也被纳入了现代版本的 JavaScript(在浏览器和 NodeJS 中)。

但在此之前,处理异步代码要复杂和困难得多。

在之前的 Python 版本中,你可以使用线程或Gevent。但是代码会复杂得多,难以理解、调试和思考。

在早期版本的NodeJS/浏览器JavaScript中,你会使用“回调函数”。这会导致回调地狱

协程

协程只是函数返回值的另一种说法,虽然听起来很专业async def。Python 知道它类似于一个函数,可以启动并在某个时刻结束,但它也可能在内部暂停(⏸),只要其中存在其他操作await

但所有这些使用异步代码的功能async通常await都被概括为使用“协程”。它与 Go 语言的主要核心特性“Goroutine”类似。

结论

让我们看看上面同样的短语:

现代版本的 Python 支持使用称为“协程”的“异步代码”,并具有相应的语法。asyncawait

现在应该更容易理解了。✨

正是这些因素驱动着 FastAPI(通过 Starlette),并使其拥有如此令人印象深刻的性能。

了解更多

此版本省略了一些 FastAPI 特有的细节。如果您想了解这些细节,包括如何在编写标准def函数和使用标准库的同时获得异步性能优势,请查阅FastAPI 文档

如果你想更深入、更专业的解释,可以看看EdgeDB的Łukasz Langa制作的这个视频系列

关于我

嘿! 👋 我是塞巴斯蒂安·拉米雷斯 ( tiangolo )。

您可以关注我、联系我、提问、了解我的工作,或者使用我的开源代码:

文章来源:https://dev.to/tiangolo/concurrent-burgers-understand-async-await-3n20