对异步编程的重新思考:Async/Await是否真的是解决方案?
我今天看到这个图:
说到async和await,它们可能是开发者对异步编程爱恨交加的原因之一。如果你让一个函数等待异步代码,那么这个函数也必须是异步的,而任何依赖它的函数同样也必须是异步的。这种层层递进的模式让代码像病毒一样被“异步化”,直到最终传递到顶层。
我对Async/Await的看法
老实说,我一直对这种模式感到厌烦。尽管它可能是目前的最佳实践,但仍然让人觉得这是种“必要之恶”。直到我开始接触Go。
Go没有async和await,但它以并发编程闻名。起初,我觉得奇怪:没有这些关键字,Go是怎么做到并发的?
举个例子,当我开发一个应用时,需要为请求增加一秒的延迟,以避免频繁访问被限制。我用Go实现延迟,只需一行代码:
time.Sleep(1*time.Second)
简单到让人难以置信。更奇妙的是,Go没有await,但延迟依然可以正常工作。这让我很好奇:如果没有await,延迟是如何实现的?它会使用效率极低的自旋锁吗?答案是否定的。
事实证明,Go的线程调度器可以高效地让线程进入休眠状态,而无需依赖复杂的await语法。这是一次颠覆性的体验。
我讨厌“聪明”的设计
我认为函数应该是简单的:接受参数、执行操作、返回结果。而像async、await这样复杂的附加机制,让代码的逻辑变得难以直观理解。
还有其他复杂机制,比如生成器。以Python为例:
deffoo():
yield1
yield2
yield3
这看似简单,但如果不了解生成器的原理,理解代码的意图就不那么容易了。在Dart中,生成器同样不算直观:
IterablesimpleGeneratorFun()sync*{
yield1;
yield2;
yield3;
}
这样的“聪明”工具似乎在告诉你,“代码逻辑比你想象的复杂”,这让我感到不适。
Go的方式
在Go中,我们用简单直接的方式处理并发。你可以通过go func()创建一个新的协程,而无需标记函数为异步。
虽然多线程可能带来复杂的错误,但async/await也无法完全解决这些问题。真正高效的解决方案可能是像Dart隔离那样的独立内存机制,而不是强制异步。
“异步病毒”与性能
异步函数不仅会“感染”其他函数,还可能拖慢性能。比如,调度器的额外参与可能反而增加开销。而某些情况下,同步代码甚至更优:Dart的lint工具avoid_slow_async_io就建议使用同步版本的文件操作,如existsSync替代exists。
反思Async/Await
如果每次获取当前时间都需要异步等待,那无疑是灾难。我们需要问自己:真的有必要强制所有异步操作显式标记为异步吗?
其实,第一种引入async/await的语言是C#,或更准确地说是F#。虽然它们确实解决了一些问题,但也将异步运行代码的复杂性推到了每一层函数中。
异步编程本身没有问题,但async/await的设计却强加了许多不必要的复杂性。它是现代编程中最大的设计失误之一。而Go的方式则证明,简单直观的异步编程并非不可实现。
结论
是时候重新审视async/await的设计了。或许我们并不需要它,甚至应该彻底抛弃它。编程世界的未来,不应该被这种模式所束缚。