自从我写过关于基于异步/等待的系统的挑战以及它们似乎不能很好地支持背压以来,已经有几年了。几年后,我不认为这个问题已经平息多少,但我的思维和理解也许已经发生了一些变化。我现在确信,对于大多数语言来说, async / await实际上是一个糟糕的抽象,我们应该瞄准更好的东西,我认为是线程。
在这篇文章中,我还将重申我之前的非常聪明的人的许多论点。这里没什么新鲜的,我只是希望把它带给新的读者群。特别是,您应该真正考虑这些极具影响力的作品:
- Bob Nystrom 的《你的函数是什么颜色》一文给出了一个非常有力的案例,即拥有两种类型的函数(仅在一个方向上兼容)会导致问题。
- Ron Pressler 的《请停止用纯粹的概念污染我们的命令式语言》 ,我认为这可能是关于该主题的最重要的演讲。
- Nathaniel J. Smith 的关于结构化并发的注释,或者:Go 语句被认为是有害的,它很好地阐述了结构化并发的动机。
您的孩子喜欢 Actor 框架
作为程序员,我们已经习惯了事物的运作方式,以至于我们做出了一些隐含的假设,这些假设确实影响了我们自由思考的能力。让我向您展示一段代码来演示这一点:
def move_mouse (): 而鼠标。 x < 200 : 老鼠。 x += 5 睡觉( 10 ) def move_cat (): 而猫。 x < 200 : 猫。 x += 10 睡觉( 10 ) 移动鼠标() 移动猫()
阅读该代码,然后回答这个问题:老鼠和猫是同时移动,还是相继移动?我向你保证,十分之十的程序员会正确地声明他们一个接一个地搬家。这是有道理的,因为我们了解 Python 以及线程、调度等概念。但如果你和一群熟悉Scratch的孩子交谈,他们很可能会得出结论:老鼠和猫同时移动。
原因是,如果您通过 Scratch 接触编程,那么您就会接触到 Actor 编程的原始形式。猫和老鼠都是演员。事实上,用户界面已经非常清楚地表明了这一点,只是演员被称为“精灵”。您将逻辑附加到屏幕上的精灵上,所有这些逻辑片段会同时运行。令人兴奋。您甚至可以在精灵之间发送消息。
我之所以想让你们思考一下,是因为我认为这是相当深刻的。 Scratch 是一个非常非常简单的系统,旨在向年幼的孩子教授编程。然而它推广的模式是演员系统!如果您想通过一本有关 Python、C# 或其他语言的传统书籍涉足编程,很可能您最终只会了解线程。不仅如此,它可能会让它听起来非常复杂和可怕。更糟糕的是,您可能只会在一些高级书籍中了解参与者模式,这些书籍会用大规模应用程序的所有复杂性轰炸您。
但您还应该记住其他一些事情:Scratch 不会谈论线程,它不会谈论 monad,它不会谈论async / await ,它不会谈论调度程序。就您作为程序员而言,它是一种命令式(尽管丰富多彩且可视化)语言,具有一些基本的消息传递“语法”支持。并发是自然而然的。孩子可以对其进行编程。这不是什么值得害怕的事情。
命令式编程并不逊色
我想让你明白的第二件事是命令式语言并不逊色于函数式语言。
虽然我们大多数人可能都在使用命令式编程语言来解决问题,但我认为我们都已经接触到这样的观念:它是低劣的并且不是特别纯粹。有一个函数式编程的世界,有 monad 和其他东西。这个世界有这些美好的事物,涉及作文、逻辑和数学以及看起来很奇特的定理。如果你用这种方式编程,你几乎已经超越到了一个更高的层面,并且俯视那些将 if 语句、for 循环缝合在一起、到处产生副作用以及用 IO 做非常不恰当的事情的人。
好吧,也许情况没那么糟糕,但我不认为我对这些氛围完全错误。看,我明白了。我很高兴将 Rust 和 JavaScript 中的 lambda 链接在一起。但我们也应该意识到,在许多语言中,这些结构都是固定的。例如,Go 不需要这些,但这并不意味着它是一种低劣的语言!
因此,您应该记住的是,存在不同的范例,并且在精神上您应该尝试暂时停止认为函数式编程已经解决了所有问题,而命令式编程却没有。
相反,我想谈谈函数式语言和命令式语言如何处理“等待”。
我想回到的第一件事是上面的例子。这两个函数(对于猫和老鼠)可以看作是单独的执行线程。当代码调用sleep(10)时,程序员显然期望计算机将暂时暂停执行并稍后继续。我不想让你对 monad 感到厌烦,所以作为我的“函数式”编程语言,我将使用 JavaScript 和 Promise。我认为这是大多数读者都足够熟悉的抽象:
函数moveMouseBlocking () { while (鼠标.x < 200 ) { 老鼠。 x += 5 ; 睡觉( 10 ); // 阻塞睡眠 } } 函数moveMouseAsync () { 返回新的Promise ((解析) => { 函数迭代() { if (鼠标.x < 200 ) { 老鼠。 x += 5 ; 睡觉( 10 )。然后(迭代); // 非阻塞睡眠 }别的{ 解决(); } } 迭代(); }); }
您可以立即在这里看到一个挑战:将阻塞示例转换为非阻塞示例非常困难,因为我们突然需要找到一种方法来表达我们的循环(或实际上任何控制流)。我们需要手动将其分解为递归函数调用的形式,这里需要调度器和执行器的帮助来完成等待。
这种风格显然最终变得足够烦人,以至于引入了async / await来主要恢复旧代码的理智。所以现在看起来更像是这样:
异步函数moveMouseAsync () { while (鼠标.x < 200 ) { 老鼠。 x += 5 ; 等待睡眠( 10 ); } }
但在幕后,什么都没有真正改变,特别是,当您调用该函数时,您只是得到一个包含“计算组合”的对象。该对象是一个承诺,最终将保存结果值。事实上,在某些语言(例如 C#)中,编译器实际上只是将其转换为链式函数调用。有了 Promise,您就可以等待结果,或者向then注册一个回调,如果该事情运行完成,就会调用该回调。
对于程序员来说,我认为async / await可以清楚地理解为某种简洁的抽象——对承诺和回调的抽象。但严格来说,它比我们开始的地方更糟糕,因为就表现力而言,我们失去了一个重要的可供性:我们不能自由地暂停。
在最初的阻塞代码中,当我们调用sleep时,我们隐式暂停了 10 毫秒;我们不能对异步调用做同样的事情。这里我们要“等待”睡眠操作。这是我们拥有这些“彩色函数”的关键方面。只有异步函数可以调用另一个异步函数,因为您不能在同步函数中等待。
停止问题
上面的示例显示了async / await导致的另一个问题:如果我们永远无法解决怎么办?正常的函数调用最终返回,堆栈展开,我们准备好接收结果。在异步世界中,必须有人在最后调用resolve 。如果它从未被调用怎么办?从理论上讲,这似乎与有人调用sleep()并长时间挂起或等待永远不会收到数据的管道没有什么不同。但不一样!在一种情况下,我们保持调用堆栈以及与其相关的所有内容都处于活动状态;在另一种情况下,我们只是有一个承诺,正在等待独立的垃圾收集,所有内容都已展开。
就合同而言,绝对没有任何规定必须调用resolve 。正如我们从理论上知道的那样,停止问题是不可判定的,因此实际上不可能知道是否有人会调用resolve。
这听起来很迂腐,但它非常重要,因为 Promise/Futures 和async / await正在使事情变得比没有它们更糟糕。让我们将 JavaScript Promise 视为最典型的示例。承诺是由匿名函数创建的,调用该函数最终会调用解析。举个例子:
让neverSettle =新Promise ((解决) => { // 该函数结束,但我们从未调用resolve });
首先让我澄清一下,这不是 JavaScript 特有的问题,但很高兴以这种方式展示它。这是完全合法的事情!这是一个永远不会兑现的承诺。这不是一个错误!承诺本身中的匿名函数将返回,堆栈将展开,我们留下一个“待处理”的承诺,最终将被垃圾收集。这是一个问题,因为它永远不会解决,你也永远无法等待它。
想想下面的例子,它稍微说明了这个问题。在实践中,您可能希望减少可以同时工作的事物数量,因此让我们想象一个可以处理最多 10 个同时运行的事物的系统。因此,我们可能想要使用信号量来发出 10 个令牌,这样最多可以同时运行 10 个事物;否则,它会施加背压。所以代码看起来像这样:
const信号量=新信号量( 10 ); 异步函数执行( f ) { 让令牌=等待信号量。获得(); 尝试{ 等待f (); }最后{ 等待信号量。释放(令牌); } }
但现在我们有一个问题。如果传递给执行函数的函数返回neverSettle会怎么样?好吧,显然我们永远不会释放信号量令牌。与阻塞函数相比,这绝对更糟糕!最接近的等价物是一个愚蠢的函数,它调用一个非常长时间运行的sleep 。但不一样!在一种情况下,我们保持调用堆栈以及与其相关的所有内容都处于活动状态;在另一种情况下,我们只是有一个最终会被垃圾收集的承诺,并且我们永远不会再看到它。在承诺的情况下,我们实际上已经确定堆栈没有用。
有一些方法可以解决这个问题,比如让 Promise 最终确定可用,这样我们就可以在 Promise 被垃圾收集时得到通知等。但是我想指出,根据合同,这个 Promise 所做的事情是完全可以接受的,我们刚刚引起了新问题,一个我们以前没有遇到过的问题。
如果你认为 Python 没有这个问题,那么它也有。只需等待 Future() ,您就会等待宇宙的热寂(或者实际上是当您关闭解释器时)。
未解决的承诺没有调用堆栈。但即使你正确使用它,这个问题也会以其他方式出现。通过调度程序流程调用函数的分解函数意味着现在您需要额外的功能来将这些异步调用拼接到完整的调用堆栈中。这一切都会产生以前不存在的额外问题。调用堆栈非常非常重要。它们有助于调试,并且对于分析也至关重要。
阻塞是一种抽象
好的,我们知道承诺模型至少存在一些挑战。还有哪些其他抽象?我将提出一个论点,一个能够“挂起”执行线程的函数是一个非常伟大的能力和抽象。想一想:无论我在哪里,我都可以说我需要等待一些事情,然后从上次中断的地方继续。如果您决定稍后需要它,这对于应用背压尤其重要。 Python 异步中最大的枪口仍然是写入是非阻塞的。该函数将永远存在问题,您需要跟进wait s.drain()以避免缓冲区膨胀。
特别是它是一个重要的抽象,因为在现实世界中,我们经常面临事实上并不总是异步的事情,并且我们认为可能不会阻塞的一些事情实际上会阻塞。就像Python在设计的时候并没有认为write应该能够阻塞一样。我想给你们举一个生动的例子。为什么以下代码会阻塞,是什么?
def解码对象( idx ): 标头=索引[ idx ] object_buf =缓冲区[标头.开始:标题。开始+标题。尺寸] 返回布罗特利。解压缩( object_buf )
这是一个有点棘手的问题,但事实并非如此。它阻塞的原因是因为内存访问可能会阻塞!您可能不会这样想,但有很多原因导致仅触及内存区域就需要时间。最明显的一个是内存映射文件。如果您正在触摸尚未加载的页面,操作系统必须将其铲入内存,然后再返回给您。没有“等待触摸这段记忆”这样的表达方式,因为如果有的话,我们就得到处等待了。这听起来可能微不足道,但阻塞内存读取是 Sentry [1]一系列事件的根源。
今天async / await所做的权衡是,并不是所有的事情都需要阻塞或挂起。然而,现实告诉我,还有更多的事情确实需要挂起,如果随机内存访问是挂起的情况,那么这个抽象还有什么价值吗?
因此,也许允许任何函数调用阻塞和挂起确实是正确的抽象。
但是接下来我们需要讨论生成线程,因为单个线程没有多大价值。 async / await系统为您提供的一种功能是您在其他情况下无法获得的,实际上是告诉两件事同时运行。您可以通过启动异步操作并将等待推迟到稍后来实现。在这里我不得不承认async / await有它的用处。它将并发执行的现实直接转移到语言中。对于 Scratch 程序员来说,并发之所以如此自然,是因为它就在那里,所以async / await在这里解决了非常相似的目的。
在基于线程的传统命令式语言中,生成线程的行为通常隐藏在(通常是复杂的)标准库函数后面。更令人烦恼的是,线程感觉非常固定,甚至完全不足以进行最基本的操作。因为我们不仅想要生成线程,还想要加入它们,我们想要跨线程边界发送值(包括错误!)。我们想要等待任务完成、键盘输入、消息传递等。
经典线程
让我们暂时关注一下线程。如前所述,我们正在寻找的是任何函数屈服/挂起的能力。这就是线程允许我们做的事情!
当我在这里谈论“线程”时,我不一定指的是线程的特定类型的实现。想一想上面的承诺示例:我们有“睡眠”的概念,但我们并没有真正说明它是如何实现的。显然有一些底层调度程序可以实现这一点,但是如何发生超出了该语言的范围。线程可以是这样的。它们可以是真实的操作系统线程,也可以是虚拟的并通过纤程或协程来实现。归根结底,作为开发人员,我们不一定需要关心语言是否正确。
这很重要的原因是,当我谈论“暂停”或“在其他地方继续”时,我立即想到协程和纤维。这是因为许多支持它们的语言都为您提供了这些功能。但最好退后一步,只考虑我们想要的一般可供性,而不是它们的实现方式。
我们需要一种方式来表达:同时运行它,但不要等待它返回,我们想稍后等待(或者永远不等待!)。基本上,在某些语言中相当于调用异步函数,但不等待。换句话说:安排函数调用。从本质上讲,这就是生成线程的含义。如果我们考虑一下 Scratch:并发性自然而然出现的原因之一是它确实集成得很好,并且是该语言的核心功能。有一种真正的编程语言,其工作原理非常相似:使用它的 goroutine。有它的语法!
所以现在我们可以产卵,然后那个东西就会运行。但现在我们有更多的问题需要解决:同步、等待、消息传递以及所有这些爵士乐都没有解决。甚至 Scratch 也有答案!很明显,还缺少其他一些东西来实现这项工作。那个生成调用甚至会返回什么?
绕道:什么是异步偶数
async / await有一个讽刺,讽刺的是它存在于多种语言中,表面上看起来完全一样,但底层的工作原理却完全不同。不仅如此,不同语言中async / await的起源故事甚至不尽相同。
我之前提到过,可以任意阻止的代码是某种抽象。对于许多应用程序来说,这种抽象只有在阻塞时的 CPU 时间可以用于其他有用的方式时才有意义。一方面,因为如果计算机只按顺序做事,它会很无聊,另一方面,因为我们可能需要并行运行。有时,作为程序员,我们需要做两件事来同时取得进展,然后才能继续。输入创建更多线程。但是,如果线程如此出色,为什么还要谈论协程和承诺,以支持不同语言中的异步/等待呢?
我认为这就是故事实际上很快变得令人困惑的地方。例如,JavaScript 面临的挑战与 Python、C# 或 Rust 完全不同。然而不知何故,所有这些语言最终都以async / wait的形式结束。
让我们从 JavaScript 开始。 JavaScript 是一种单线程语言,函数作用域无法产生。该语言没有能力做到这一点,并且线程也不存在。所以在async / await之前,你能做的最好的就是不同形式的回调地狱。改善这种体验的第一次迭代是添加承诺。 async / await后来才变成了糖。 JavaScript 在这里没有太多选择的原因是,promise 是唯一可以在不改变语言的情况下完成的事情,而async / await是可以作为转译步骤实现的东西。真的; JavaScript 中没有线程。但这里发生了一件有趣的事情:JavaScript 在语言层面上有并发的概念。如果调用setTimeout ,则告诉运行时安排稍后调用的函数。这一点至关重要!特别是,它还意味着创建的承诺将被自动安排。即使你忘记了它,它也会运行!
另一方面,Python 有一个完全不同的起源故事。在async / await之前的日子里,Python 已经有了线程——真正的操作系统级线程。然而,它不具备多个线程并行运行的能力。原因显然是 GIL(全局解释器锁)。然而,“只是”使得事情不能扩展到多个核心,所以让我们暂时忽略这一点。因为它有线程,所以很早就有人尝试在 Python 中实现虚拟线程。过去(以及今天的某种程度上)操作系统级线程的成本相当高,因此虚拟线程被视为产生更多并发事物的快速方法。 Python 获得虚拟线程的方式有两种。其中一个是 Stackless Python 项目,它是 Python 的替代实现(很多是 cpython 的补丁),它实现了所谓的“stackless VM”(基本上是不维护 C 堆栈的 VM)。简而言之,这实现了一种称为“tasklet”的无堆栈功能,这些功能可以暂停和恢复。 Stackless 没有光明的未来,因为无堆栈的性质意味着您无法交错 Python -> C -> Python 调用并在堆栈上挂起它们。
Python 中还有第二次尝试,称为“greenlet”。 greenlet 的工作方式是在自定义扩展模块中实现协程。它的实现相当粗糙,但它确实允许协作多任务处理。然而,就像 stackless 一样,这并没有胜出。相反,实际发生的情况是,Python 多年来的生成器系统逐渐升级为具有语法支持的协程系统,并在此基础上构建了异步系统。
这样做的后果之一是它需要语法支持才能从协程中挂起。这意味着您无法实现像sleep这样的函数,该函数在调用时会产生调度程序。您需要等待它(或者在早期您可以使用yield from )。因此,由于协程在 Python 中的底层工作方式,我们最终选择了async / await 。这样做的动机是,当某些事情暂停时你知道它被视为一件积极的事情。
Python 协程模型的一个有趣的结果是,至少在协程模型上它可以超越操作系统级别的线程。我可以在一个线程上创建一个协程,将其发送到另一个线程,然后在那里继续。实际上,这是行不通的,因为一旦与 IO 系统连接,它就无法再进入另一个线程上的另一个事件循环。但您已经可以看到,从根本上来说,它所做的事情与 JavaScript 完全不同。至少在理论上它可以在线程之间移动;有线程;有语法可以产生。 Python 中的协程一开始也不会运行,这与 JavaScript 不同,在 JavaScript 中它总是被有效地调度。这也部分是因为 python 中的调度程序可以被换出,并且存在竞争和不兼容的实现。
最后我们来谈谈C#。这里的起源故事再次完全不同。 C# 有真正的线程。它不仅具有真正的线程,还具有每个对象的锁,并且在处理并行运行的多个线程时绝对没有问题。但这并不意味着它不存在其他问题。现实情况是,仅靠线程是不够的。您需要经常在线程之间进行同步和对话,有时您只需要等待。例如,您需要等待用户输入。当你被困在那里处理输入时,你仍然想做一些事情。因此,随着时间的推移,.NET 引入了“任务”,它是异步操作的抽象。它们是 .NET 线程系统的一部分,您与它们交互的方式是在其中编写代码,您可以使用语法暂停任务。 .NET 将在当前线程上运行任务,如果您执行一些阻塞操作,您将保持阻塞状态。从这个意义上说,这与 JavaScript 完全不同,在 JavaScript 中,虽然没有创建新的“线程”,但您会在调度程序中挂起执行。它在 .NET 中以这种方式工作的原因是,该系统的部分动机是允许 UI 触发代码访问主 UI 线程而不阻塞它。但结果又是,如果你真的阻止了,你就搞砸了。然而,这也是为什么 C# 至少在某一时刻所做的只是在遇到await时将函数拼接到链式闭包中。它只是将一段逻辑代码分解为许多单独的函数。
我真的不想深入了解 Rust,但 Rust 的异步系统可能是其中最奇怪的,因为它是基于轮询的。简而言之:除非你主动“等待”任务完成,否则它不会取得进展。因此,调度程序的目的是确保任务确实能够取得进展。为什么 Rust 以async / await结尾?主要是因为他们想要一些无需运行时和调度程序以及借用检查器和内存模型的限制即可工作的东西。
在所有这些语言中,我认为对于 Rust 和 JavaScript 来说, async / await的争论最为强烈。 Rust 因为它是一种系统语言,他们想要一种可以在有限的运行时间下工作的设计。 JavaScript 对我来说也很有意义,因为该语言没有真正的线程,因此async / await的唯一替代方案是回调。但对于 C# 来说,这个论点似乎要弱得多。即使必须强制代码在 UI 线程上运行的问题也可以通过为虚拟线程制定调度策略来解决。在我看来,最糟糕的罪犯是 Python。 async / await最终形成了一个非常复杂的系统,其中该语言现在具有协程和真正的线程,每个协程有不同的同步原语,以及最终被固定到一个操作系统线程的异步任务。该语言甚至在线程和异步任务的标准库中具有不同的未来!
我想让你理解这一切的原因是,所有这些不同的语言都共享相同的语法,但你可以用它做的事情却完全不同。它们的共同点是异步函数只能由异步函数(或调度程序)调用。
异步不是什么
多年来,我听到了很多关于为什么 Python 最终会使用async / await的争论,而从我的角度来看,提出的一些论点经不起推敲。我反复听到的一个论点是,如果您控制何时挂起,则不需要处理锁定或同步。虽然这有一定道理(你不会随机暂停),但你最终仍然必须锁定。仍然存在并发性,因此您仍然需要保护所有内容。特别是在 Python 中,这尤其令人沮丧,因为不仅有彩色函数,还有彩色锁。有线程锁和异步代码锁,它们是不同的。
我展示上面的信号量示例有一个很好的理由:信号量在异步编程中是真实存在的。它们经常被用来保护系统免于承担过多的工作。事实上,许多基于async / await的程序所面临的核心挑战之一是缓冲区膨胀,因为无法施加背压(我再次向您指出我的帖子)。为什么他们不能?因为除非 API 是async ,否则它会被迫缓冲或失败。它不能做的就是阻止。
异步也不能神奇地解决 Python 中 GIL 的问题。它不会神奇地使真正的线程出现在 JavaScript 中,也不会解决随机代码开始阻塞时的问题(请记住,即使是内存访问也可能会阻塞)。或者你非常缓慢地计算一个大的斐波那契数。
答案是线程,而不是协程
我已经在上面多次提到过这一点,但是当我们想到能够从任意时间点“挂起”时,我们通常会立即将协程视为程序员。有充分的理由:协程令人惊奇,它们很有趣,并且每种编程语言都应该拥有它们!
协程是一个重要的构建块,如果未来的语言设计师正在查看这篇文章:您应该将它们放入其中。
但协程应该非常轻量级,而且它们可能会被滥用,导致很难跟踪正在发生的事情。例如,Lua 为您提供了协程,但它没有为您提供轻松使用它们执行某些操作所需的结构。您最终将构建自己的调度程序、自己的线程系统等。
所以我们真正想要的是我们开始的地方:线程!好老线程!
具有讽刺意味的是,我认为真正正确的语言是现代 Java。 Java 中的Project Loom具有协程和所有底层功能,但它向开发人员公开的是良好的旧线程。有虚拟线程,它们安装在运营商操作系统线程上,并且这些虚拟线程可以在线程之间移动。如果您最终在虚拟线程上发出阻塞调用,它将交给调度程序。
现在我碰巧认为仅靠线程还不够好!线程需要同步,它们需要通信原语等。Scratch 具有消息传递!因此,需要构建更多东西才能使它们正常工作。
我想跟进另一篇博客文章,了解如何使线程更易于使用。因为async / await 的明显创新是使其中一些核心功能更接近该语言的用户,并且通常现代的async / await代码看起来比使用线程的传统代码更容易阅读。
结构化并发和通道
最后,我确实想说一些关于async / await 的好话,并庆祝它带来的创新。我相信,这种语言功能通过使其广泛可访问性,单独推动了并发编程的一些关键创新。特别是,它使许多开发人员从基本的“每个请求单线程”模型转向将任务分解为更小的块,甚至在 Python 等语言中也是如此。对我来说,最大的创新来自Trio ,它通过其托儿所引入了结构化并发的概念。这个概念最终甚至在 asyncio 中与TaskGroup API的概念找到了归宿,并且正在进入 Java 。
我建议您阅读 Nathaniel J. Smith 关于结构化并发的注释,或者:Go 语句被认为是有害的,以获得更好的介绍。但是,如果您不熟悉它,我将尝试对此进行解释:
- 有明确的工作开始和结束:每个线程或任务都有明确的开始和结束,这使得更容易跟踪每个线程正在做什么。在线程上下文中生成的所有线程对该线程都是已知的。可以把它想象成创建一个小团队来完成一项任务:他们一起开始,一起完成,然后报告。
- 线程的寿命不会比父线程长:如果出于某种原因父线程在子线程之前完成,它会在返回之前自动等待。
- 错误传播并导致取消:如果一个线程中出现问题,错误将传递回父级。但更重要的是,它还会自动导致其他子线程取消。取消是系统的核心!
我相信结构化并发需要成为线程世界中的一个事物。线程必须知道它们的父母和孩子。线程还需要找到方便的方法来传回其成功值。最后,上下文应该通过上下文局部变量从一个线程隐式地流到另一个线程。
第二部分是async / await使得任务/线程需要相互通信变得更加明显。特别是渠道和渠道选择的概念变得更加普遍。这是一个必不可少的构建块,我认为可以进一步改进。作为思考的食物:如果您已经结构并发性,则原理可以将每个线程的返回值真正表示为附加到线程的缓冲通道,最多可以选择一个值(成功的返回值或错误),您可以选择。
今天,尽管没有任何语言完善该模型,但由于多年的实验,该解决方案似乎比以往任何时候都更加清晰,其结构化并发性的核心。
结论
我希望我能够向您证明异步/等待是一个混合的包。它给了回调地狱带来了一些缓解,但它也使我们感到不安,诸如彩色功能,新的背压挑战,并完全引入了全部诺言,例如可以永远坐下来而无需解决的诺言。它还夺走了Call堆栈带来的大量实用程序,尤其是用于调试和分析。这些不是小打ic;它们是真正的障碍,妨碍了我们应该追求的直接,直观的并发。
如果我们退后一步,我似乎很清楚,我们通过采用异步/等待具有真实线索的语言来转向课程。诸如Java的Project Loom之类的创新感觉就像在这里合适。虚拟线程在需要时可以产生,在阻塞时切换上下文,甚至可以使用使并发感到自然的消息系统。如果我们摆脱了功能性,承诺系统已经弄清了我们可以再次正确查看线程的所有问题。
但是,同时,异步/等待的同时编程已转移到最前沿,并导致了真正的创新。将并发作为语言的核心特征(即使是语法!)是一件好事。也许收养和人们挣扎的人们的挣扎是使结构化并发成为现实中的真正事物的原因。
未来的语言设计应该再次重新考虑并发性:新语言不再采用异步/等待异步,而是像Java的项目织机一样建模,但具有更友好的原始图。但是,就像划痕一样,它应该为程序员提供真正自然而然的API。我认为演员框架不是正确的,但是结构化并发,频道,语法支持产卵/连接/选择的语法支持将有很长的路要走。观看此空间以获取以后的博客文章,内容涉及我发现的某些事情比其他事情更好。
[1] | Sentry可以使用大型调试信息文件,例如PDB或矮人。这些文件的大小可以是千兆字节,我们的内存映射预处理文件的terrabyte在处理过程中的内存中。内存映射的文件可能会屏蔽并不令人惊讶,但是我们在此过程中学到的是,由于集装箱和内存限制,您可以轻松地将自己导航到您在页面故障上花费更多时间的情况,并且系统爬网停止。 |
原文: http://lucumr.pocoo.org/2024/11/18/threads-beat-async-await