问题

为什么编程语言对异步编程都是很晚近才开始支持的?

回答
这个问题很有意思,也很切中要害。确实,你看现在像 JavaScript、Python、Java、C 等主流语言,都在过去十几年里纷纷引入或大大增强了对异步编程的支持,什么 `async/await`、`Promise`、`CompletableFuture`、`Task`,层出不穷。但这就像是人们突然意识到缺了点什么,然后一窝蜂地开始补课一样,感觉很像是“新潮流”。

那为什么会这样呢?这背后其实牵扯到计算机发展的历史、硬件的演进、软件设计的理念,以及早期编程语言设计时的侧重点。要说清楚这一点,咱们得一层一层地剥开来看。

1. 计算的本质与同步的“顺理成章”

首先,我们得回到计算机最基础的工作方式。在早期的计算机设计和编程思维里,最直观、最容易理解的模型就是顺序执行。 CPU 拿到一条指令,执行它;拿到下一条,执行它。就像一个流水线,一步一步往前走。

当我们要处理一个任务时,自然而然地就会想到,我把这个任务拆解成一系列步骤,然后按顺序告诉我计算机怎么做。这种模型叫做同步编程 (Synchronous Programming)。你发出一个请求(比如读文件、发网络请求),然后就乖乖地等着,直到这个请求完成,你才能进行下一步。在这个过程中,CPU 的大部分时间可能都在等待 I/O 操作(输入/输出,比如硬盘读写、网络通信)完成,它什么也干不了,就“卡”在那里了。

这种同步的模型非常符合人类的线性思维习惯,也容易实现。程序员可以清晰地知道代码的执行顺序, debug 也相对直接。因此,早期的编程语言设计者们,他们设计的语言自然就围绕着这种同步模型展开。你写一个函数,调用它,它返回结果,就这么简单。

2. 硬件进步与瓶颈的显现

接着,我们看看硬件怎么发展的。早期计算机的 CPU 速度相对于 I/O 设备(如磁盘、网络)来说,差距没那么大。但随着摩尔定律的推进,CPU 越来越快,而 I/O 设备的发展速度相对滞后。这导致了一个巨大的性能瓶颈:CPU 飞快地运转,结果却因为要等待一个慢吞吞的磁盘读写或者网络响应而不得不停下来。

这种等待对用户体验来说是灾难性的。想想看,一个网页加载,如果每个资源(HTML、CSS、JS、图片)都要等上一个完成才能加载下一个,那网站会慢到什么程度。同样,一个服务器如果每处理一个请求都要等 I/O 完成,那么它能同时服务的用户数量就会非常有限。

3. 应对瓶颈的早期尝试:回调与事件驱动

在语言层面正式支持异步编程之前,程序员们其实一直在用各种“土办法”来绕过同步模型的阻塞问题。其中最早期也最普遍的一种方式就是回调函数 (Callback Functions)。

它的思想是:当我发起一个异步操作时(比如 `readFileAsync(filePath, callbackFunction)`),我不等你完成,而是直接继续做别的事情。当这个异步操作完成后,它会“通知”你,然后调用你之前提供的那个 `callbackFunction` 来处理结果。

这种方式在某些场景下确实有效,比如 Node.js 的早期设计就大量依赖回调。但是,回调的缺点非常明显:

回调地狱 (Callback Hell):当一个操作需要依赖前一个操作的结果,然后又发起新的异步操作,层层嵌套下去,代码会变得非常难以阅读和维护,就像陷入了地狱一样。
错误处理困难:异步操作的错误处理需要特别小心,很容易遗漏。
逻辑流程不直观:代码的执行顺序不再是线性的,理解起来更费劲。

除了回调,还有事件驱动 (EventDriven) 的编程模型。这是一种更广泛的概念,核心思想是程序等待事件发生(比如用户点击按钮、数据到达网络缓冲区),然后根据事件类型触发相应的处理函数。像早期的 GUI 编程、服务器端的 Socket 编程都大量运用了事件驱动的概念。但很多时候,这些事件处理本身仍然可能是同步的,或者需要配合回调来处理异步操作的结果。

4. 并发与多线程的引入与挑战

除了异步 I/O,提升性能的另一个思路是并发 (Concurrency) 和并行 (Parallelism)。简单来说:

并发是指同时处理多个任务,但这些任务的执行可能交错进行,不一定是真正意义上的“同时”。
并行是指多个任务真正意义上同时执行,这通常需要多核 CPU。

为了实现并发,编程语言引入了线程 (Threads) 的概念。一个进程可以有多个线程,这些线程可以同时(在多核 CPU 上)或者交替(在单核 CPU 上)执行。

然而,多线程编程也带来了新的巨大挑战:

竞态条件 (Race Conditions):多个线程同时访问和修改共享数据时,结果可能取决于线程执行的顺序,导致不可预测的错误。
死锁 (Deadlocks):两个或多个线程互相等待对方释放资源,导致程序永远无法继续执行。
资源管理复杂:线程的创建、销毁、同步(如锁、信号量)都非常复杂,容易出错。
上下文切换开销:操作系统在不同线程之间切换时有额外的开销。

因此,虽然多线程可以利用多核 CPU,但它引入的复杂性让很多开发者望而却步,而且对于大量的 I/O 操作,单纯依靠多线程并不能完全解决阻塞问题(因为每个线程仍然需要等待,只是同时有多个线程在等待)。

5. 终于等来了,异步的“优雅姿势”:协程、Promise/Future、async/await

在经历了回调的混乱和多线程的痛苦之后,开发者们越来越渴望一种更简单、更直观的方式来处理异步操作,同时又能避免线程的复杂性。这就催生了现代异步编程模型:

协程 (Coroutines):这是一种更轻量级的并发模型,它允许函数暂停自己的执行,并在稍后从暂停处恢复。协程可以看作是一种用户态的线程,它们由程序本身(而不是操作系统)来调度。Python 的 `asyncio`、Go 的 Goroutines 就是很好的例子。协程的核心优势在于,在协程内部,调用一个耗时的异步操作时,代码看起来仍然像同步一样,但实际底层是将执行权交给了调度器,去执行其他协程。

Promise (JavaScript) / Future (Java/C):这些都是代表了“未来某个时间点会完成的操作”的对象。当你发起一个异步操作时,它会立即返回一个 Promise 或 Future 对象。你可以给这个对象附加“当操作完成时要执行的回调”或者“当操作失败时要执行的回调”。这比纯回调更结构化,也更容易链式调用,从而缓解了回调地狱。

async/await 语法糖:这是对 Promise/Future 模型的一种更高级的抽象。它让异步代码写起来几乎和同步代码一样直观。你可以在一个异步函数前加上 `async` 关键字,然后在函数内部使用 `await` 关键字来等待另一个异步操作的完成。`await` 会自动处理暂停和恢复的逻辑,并返回异步操作的结果。这极大地提升了异步编程的可读性和易用性。

6. 为什么是“晚近”才流行?

综合以上几点,我们可以看出为什么异步编程的成熟支持是“晚近”才普及的:

1. 历史原因:早期计算机和编程语言设计时,同步模型是主流且容易理解,没有迫切的需求去解决异步带来的复杂性。
2. 硬件瓶颈凸显:随着硬件发展,CPU 速度远超 I/O,异步 I/O 的重要性才变得日益突出。
3. 早期解决方案的不足:回调虽然能解决问题,但体验很差;多线程虽强大,但复杂度和风险太高。
4. 技术成熟度与抽象的演进:协程、Promise/Future、async/await 等更优雅、更易用的异步模型,是经历了多年的探索和实践后,逐步发展和成熟起来的。语言设计者们看到了这种需求,并提供了更高级别的抽象来降低异步编程的门槛。
5. 生态系统的支持:随着 Node.js、WebAssembly 等技术的发展,异步编程在前端和后端都变得至关重要,推动了语言层面和框架层面对异步的支持。

所以,与其说编程语言“晚近才支持”异步编程,不如说是因为异步编程的模式和工具,直到最近才发展到足够成熟、足够易用,能够真正大规模地普及和被开发者们广泛接受。这就像一项技术,不是它不存在,而是直到找到了最适合它的“表达方式”和“实现工具”,才能真正发扬光大。

网友意见

user avatar

因为“直接在语言层面支持异步”并不是必需的;尤其是在经过大量实践、真正把各种异步模型的优缺点彻底摸清之前,“语言层面的异步支持”反而是笨拙的、多余的。


异步模型本来就有很多很多种。从早期的中断服务模型、多进程协同模型再到轻量级进程、线程乃至协程,业界也走过了很多弯路。

比如,早期的Windows 3.X搞的协作时多任务就是协程思路管理的进程,在开发者良莠不齐甚至抱有敌意的环境下这么搞完全是自寻烦恼;于是到了Windows 95就改用了“抢夺式多任务”调度方案。

再比如,线程刚刚兴起时,Linus坚持认为Linux的进程已经足够用了,而且还有轻量级进程可以用;这致使Linux有很多年都不能支持线程(可以通过库来支持,但库做不到“一个进程内的多个线程同时利用多个CPU核心”:其实不怕麻烦的话,用进程来实现线程、用共享内存模拟“可共享的进程内资源”,也还是可以写出“有多CPU支持的‘线’程库”的,但那就有点行为艺术了)。


那时只有服务器才较多使用了多CPU主板——注意和现在的多核心不同,多CPU主板上面有两个以上的CPU插槽,允许插多颗物理CPU;而且多颗物理CPU各自有自己的内存,不同CPU之间要访问对方的内存就必须通过进程间通讯机制挤总线传输数据。

那么,共享进程资源的线程显然并不能从中得到任何好处——因此,Linus的决定显然极有道理。


但是,CPU频率提升遇到了瓶颈,多核多线程时代来临。

“多线程CPU”指的是单个CPU核心内部制作了超量的逻辑单元,比如多个ALU、多个取指/译码电路,等等;这种CPU除非跑MMX/SSE/AVX之类指令,否则内部的大部分逻辑单元是不可能被充分利用的;但如果你有两条线程,它们就可以较为充分的利用这一颗CPU核心内部的大量逻辑单元了。

“多核CPU”则是把多个CPU核心封装在同一块芯片内部,物理上看是一颗CPU,插在只有一个CPU插槽的主板上、使用同一组内存条;但实际上,这颗CPU内部有若干个物理的CPU核心,你完全可以给它们分别安排不同的任务……

不仅如此。

当年AMD最早把两颗CPU核心做到同一片硅晶片上,首先推出了双核CPU;Intel仓皇应对,把两颗奔腾D晶片封装进去,也推出了自己的双核产品——后者被网友调侃为“胶水双核”。双方很是打了一番口水战。


实际上,当年的竞争还要激烈得多,复杂得多。

比如,RISC和CISC之争:CISC太复杂了,为了兼容,甚至连8086的指令都能在Pentium上跑,这得浪费多少芯片面积、给编译器造成多少麻烦……RISC决定抛掉兼容性包袱,精心优化指令集,限制缓慢的内存访问指令的数量、把节省下来的芯片面积多造寄存器、通过寄存器窗口切换寄存器组,使得函数调用/线程切换不影响执行效率;同时由于它的指令集更简洁,编译器优化就更容易……

又有人说,我们干嘛不在一颗CPU里面造很多很多逻辑单元呢?然后在编译器上下功夫,把大量用户操作整合进一条指令——就好像“背包问题”一样,一条指令整合尽可能多的操作、尽可能的充分利用CPU资源……这不就可以最大限度的提升指令执行效率了吗?

这就是所谓的“超长指令字计算机”。

此外,还有超级标量CPU等很多奇思妙想。这些东西都失败了;但这并不等于说它们都是错的;相反,它们仅仅是“商业上未能取得成功”而已;它们的思路还是被现代CPU汲取、整合进来了。比如,指令多发射、SIMD以及RISC的很多先进经验,现在都整合在x86里面了。


显然,CPU架构该如何发展,就连Intel/AMD都说不准。人类的能力,只能做到“走一步看一看”“提出一大堆看起来很美好的理论,到市场上比比优劣”,然后淘汰掉不合时宜的、保留其中更为优秀的。

甚至于,很多东西的走向仅仅决定于一个偶然——比如,如果不是AMD逼迫,或许intel就不会搞“胶水双核”方案;那么现在多核CPU的核心之间的联系可能就会紧密得多,甚至直接走“超长指令字计算机”的路子都有可能。

但一旦“胶水双核”的框架出来了、片内总线、共用cache以及有效性算法等等发展起来了、适应这个架构的高级指令集设计出来了,CPU的发展路径就被固定到现在这个模式上了。


类似的,显卡搞通用计算、做GPGPU深入人心了,那么CPU上面再搞AVX就要受限了——小打小闹你可以靠低延迟获胜;但真玩大的……怎么可能玩的过GPU?

但另一方面,CPU做核芯显卡,直接整合个GPU进去、然后再把GPU计算单元和CPU内部单元无缝融合……就好像80386CPU和80387数学协处理器被整合进486CPU一样,这会不会是未来的发展方向呢?


回到问题:在多核多线程架构完全确立下来之前,你想让编程语言如何支持异步编程呢?

在相关领域的研究/实践足够多、方案足够成熟之前,你连“异步究竟应该做成‘协作式多任务’还是‘抢占式多任务’”都不可能知道——async/await并不是表面看来那样,仅仅是一个简单的关键字;它的背后必需存在一个合理的体系,一个异步执行框架,不然就没法实现功能。


显然,过早的和一个不成熟的方案绑定(比如你的语言特性完全绑定于Windows3.1的协作式模型),只会让你的心血跟着这个不成熟的方案一起付诸东流。

因此,在时代来临之前,在编程语言中添加“异步编程的支持”,显然是有百害而无一利的。

这种东西就应该用库支持。将来架构发展方向变了,也就是废掉一个库的问题,不会拖死一整个语言,对吧。


但到了现在,由于硬件架构发展方向越发清晰、固定,我们终于可以确定“异步编程模型”应该是什么样子了:

1、最上层是进程;进程是持有资源的最小单位

2、中层是线程;线程不持有资源,是CPU调度的最小单位

3、下层是协程;协程既不持有资源、也不必在意CPU调度,它仅仅关注“协作式的、自然的执行流程切换”


当然,细节肯定还是会千变万化的;但大致来说这个整体图景不太可能有大的变动了。


底层稳定下来了,语言的直接支持才可能跟上。

不然的话,你见过哪门编程语言换个新一代的CPU就得禁用若干个关键字、或者把某些关键字的含义改变一番的?

python 2 to 3不过是风格上的少许改变,至今都还鸡飞狗跳的。它要告诉你“因为windows 95上进程的含义和Windows 3.1有所不同,因此你必须检查你的程序,在如下(省略五千字)情况下,请不要使用async关键字”或者“由于zen4改用了超长指令字架构,await在如下(省略一万字)情况下无法正常工作”,你还不得去刨Guido的祖坟啊。


反过来说也对:一旦大部分语言和OS和CPU的某个特性绑定,那么OS和CPU就没法改变设计了。比如,如果现在有人提出了一个比线程/进程模型更优越的新架构,OS/CPU制造商就不得不在“抛弃一部分语言和它们的用户”和“使用新架构但给出一个性能有所损失的兼容层”和“挺好的,但……算了,雪藏起来吧”之间做出选择。


再换句话说:程序设计语言本就应该和OS和CPU的具体实现脱耦;在进程-线程-协程模型确立之前,提供async/await就产生了“与OS或CPU的紧密耦合”,无论对编译器商还是对OS开发者还是CPU制造者,这都是个极大的不利。


注意这是“条件不成熟”,并不是什么“滞后”。

举例来说,AIO至今没有一个统一的方案(Windows仍然坚持自己的‘完成端口’,Linux则继续它的epoll,而BSD觉得kqueue挺好的);因此没有任何语言提供AIO的直接语法支持——你完全可以用诸如libevent之类库写出跨平台的高性能网络服务程序;这样将来倘若有人搞出来一个真正天才的、足以一统天下的方案,你只需提供一个接口兼容的转接层,原本的程序就仍然能运行。

但让某种语言直接提供支持?将来必定是天下大乱:改变语义,原有的项目统统死掉;内部做判断、给新项目用新语义,无论编译器还是程序编写的工作都会变得极其复杂、易错。

想想新的入门者必须区分3.7.13之前和之后的版本,不同版本await关键字含义大体相同但又有微妙的差别……

这也太疯狂太不负责任了,对吧。

user avatar

讲点儿别的……

你有两个前提搞错了

一是对异步的需求

一是编程语言的发展与变化



这两者都是近期才出现的

异步的需求是随着互联网时代才变得重要的

编程语言的蓬勃发展是虚拟机和中间语言成熟后才发生的

类似的话题

  • 回答
    这个问题很有意思,也很切中要害。确实,你看现在像 JavaScript、Python、Java、C 等主流语言,都在过去十几年里纷纷引入或大大增强了对异步编程的支持,什么 `async/await`、`Promise`、`CompletableFuture`、`Task`,层出不穷。但这就像是人们突.............
  • 回答
    这种差异,与其说是现代编程语言对 `null` 的“深恶痛绝”,不如说是对不同类型错误的不同理解和应对策略。究其根本,是因为 `0` 和 `null` 在概念上、在程序运行过程中以及在开发者意图上,扮演着截然不同的角色。让我们从 `0` 开始聊。数字 `0`,在数学和逻辑上,是一个非常具体、有意义的.............
  • 回答
    你提的这个问题挺有意思的,也确实是很多人都关注的一个现象。你可能会觉得,怎么好像身边做编程或者软件工程的女生不多呢?这背后其实有很多复杂的原因,不是一句话就能说清楚的。首先,咱们得承认,从小到大,很多女孩子接触到的信息、被鼓励去尝试的领域,和男孩子可能就不太一样。你看市面上很多玩具,比如机器人、电子.............
  • 回答
    编程这事儿,说起来挺玄乎,但归根结底就是和机器打交道,让它按照你设想的逻辑运转起来。你觉得难,这太正常了,不是你一个人这样,绝大多数人刚开始接触编程,都会碰得头破血流。至于为什么难,我觉得是思维方式和学习方式两者都有问题,而且是相互影响的。咱们先聊聊思维方式。编程这玩意儿,最核心的就是逻辑。你得把一.............
  • 回答
    关于“漂亮”的编程语言语法,这确实是个很有趣的话题,因为它本身就带着很多主观色彩,就像评价一幅画的美丑一样。但如果非要我聊聊我对“漂亮”语法的理解,那大概是这样一番感受:首先,我认为一个漂亮得体的编程语言语法,首先要做到的是清晰且易于理解。它应该像一封写得条理分明、字迹清秀的信,让你一眼就能明白作者.............
  • 回答
    这真是个好问题,它触及了现代计算机体系结构的核心奥秘之一:分支预测。你观察到的现象非常有道理:如果一段代码经常会执行某个分支,岂不是可以想办法“优化”一下,让 CPU 更“聪明”地猜对?要回答这个问题,我们得先从 CPU 的工作原理聊起,尤其是它如何处理我们写的代码。CPU 的“加速之道”:流水线和.............
  • 回答
    这个问题触及了计算机科学的核心,也是许多开发者在职业生涯中会反复思考的。为什么世界不是像我们期待的那样简单,只有一个完美的工具包揽一切?实际上,编程语言的丰富多样,恰恰是技术发展、人类需求以及对“最优解”不断探索的生动体现。想象一下,如果我们只有一个尺子,它只能测量厘米,但我们要加工一块木头,需要精.............
  • 回答
    很多时候,人们会问,为什么我们编程用的语言,比如 C、Java、Python,它们的语法规则,都可以用“上下文无关文法”(ContextFree Grammar, CFG)来描述,为什么不能更进一步,用“上下文有关文法”(ContextSensitive Grammar, CSG)来定义呢?这背后其.............
  • 回答
    这个问题很有意思,因为它触及了编程语言的本质以及人类认知和沟通的根本差异。简单来说,编程语言和自然语言之所以存在巨大鸿沟,并且后者向前者靠拢的步伐显得缓慢,不是因为设计者们不愿意,而是因为两者承担的“任务”和遵循的“逻辑”截然不同,强行融合反而会弊大于利。首先,我们要理解编程语言的终极目标是什么。它.............
  • 回答
    编程语言就像是不同领域的巧匠,它们各有专长,也各有不擅长之处,这背后有着深刻的原因,是历史演进、设计哲学以及技术需求的共同塑造。你想啊,世界上最初并没有“编程语言”这个概念,人们只能用最底层的机器指令跟计算机沟通,那简直是天书,写点什么都困难无比。后来,为了让人类更容易理解和操作,就有了汇编语言,它.............
  • 回答
    这个问题很有意思,也触及到了编程语言设计背后的一些历史渊源和现实考量。要说为什么现在编程语言主要用拉丁字母而不是片假名,我们可以从几个方面来聊聊。1. 历史的惯性与技术先行者首先得认识到,现代计算机科学和编程语言的早期发展,很大程度上是在英语为主要语言的国家进行的。美国和欧洲是那个时代的科技中心,像.............
  • 回答
    你这个问题问得很有意思,触及到了编程语言设计中的一个基础且普遍的约定:为什么赋值的变量总是出现在左边?这背后确实有着历史的沉淀和设计上的考量,并非偶然。要理解这一点,咱们得回到编程的源头,看看早期计算机是如何工作的。那时候,编程可不像现在这么直观,很多概念都是从物理和数学的运作方式中演化而来的。从物.............
  • 回答
    想象一下,你在跟一位讲究效率的口译员交流。你说的每一句话,都需要他一个词一个词地辨别、消化,然后才能翻译。为了让他清楚地知道你这句话说完了,你可以通过语调的停顿、结尾的词语,甚至一个眼神来示意。编程语言中的分号,就像是那个清晰的“句号”,是告诉计算机:“我这部分指令已经说完了,可以处理下一部分了。”.............
  • 回答
    说起上古编程语言,比如COBOL,它们的代码为何总是清一色的大写字母,这背后其实藏着一段关于计算、显示技术以及软件工程发展早期的一些有趣故事。要理解这一点,我们得跳出如今我们习以为常的彩色高亮、智能补全的现代IDE(集成开发环境),回到那个没有那么多花哨工具的时代。首先,要明白的是,那个时候的计算机.............
  • 回答
    坦白说,这个问题触及了编程语言设计领域一个相当核心的选择,而这个选择背后,是历史、可读性、以及开发者习惯的复杂交织。之所以很多语言选择了花括号 `{}` 来界定代码块,而非像 Python 那样依赖缩进,可以从几个角度来深入理解。首先,我们得认识到,花括号作为一种“显式”的标记,它提供了一种非常直观.............
  • 回答
    这背后其实是一套相当精密的计算逻辑,跟计算机处理数据的方式息息相关。你想啊,计算机内部处理信息,最基础的就是内存。内存就像一个长长的、首尾相连的仓库,里面一格一格的存放着数据。当我们说一个数组,比如有5个元素的数组,在内存里它就占用了一连串连续的空间。最关键的是,计算机需要一种方法来快速地找到这个数.............
  • 回答
    生活中的事物,你想让它是什么样子,它基本上就得是什么样子,比如你想让桌子长得方方正正,它就得方方正正,你不可能指望它突然长成一个圆柱体。编程语言里的变量类型,说白了,就是给数据规定一个“形状”,或者说“属性”,让它按照我们设定的规则来运作。没有这个“形状”的概念,计算机就像一个完全没有概念、什么都混.............
  • 回答
    这就像问为什么世界上有成千上万种食谱,但大家日常最常做的还是那几样家常菜一样。原因嘛,说起来也是一连串的现实考量,而不是什么神秘的预言。首先,得谈谈“效率”。程序员也是人,要吃饭,要养家,要在这个世界上生存。学习一门新的编程语言就像学习一门外语,或者说,学习一项新的复杂技能。这中间需要投入大量的时间.............
  • 回答
    Prolog 作为一种逻辑式编程语言,在学术界和特定领域(如人工智能、自然语言处理、专家系统、数据库查询等)有着深远的影响和不少忠实的支持者,但它确实没有像 C、Java、Python 那样成为一种主流的、被广泛应用的通用编程语言。这背后有多方面的原因,我们可以从以下几个维度来详细探讨: 1. 编程.............
  • 回答
    说起 C 语言风格的 `for` 语句,相信不少程序员都会在脑海中勾勒出那个经典的 `for (初始化; 条件; 更新)` 的样子。它简洁、强大,支撑起了无数的软件系统。然而,我们确实能观察到一个有趣的现象:许多近年出现的编程语言,在设计上似乎都选择“绕开”或者“重新诠释”这种 C 式 `for`。.............

本站所有内容均为互联网搜索引擎提供的公开搜索信息,本站不存储任何数据与内容,任何内容与数据均与本站无关,如有需要请联系相关搜索引擎包括但不限于百度google,bing,sogou

© 2025 tinynews.org All Rights Reserved. 百科问答小站 版权所有