为什么Java没有async/await?

使用 async/await,可以以命令式样式对异步函数进行编码。这可以极大地促进异步编程。在C#中首次引入后,它被许多语言所采用,例如JavaScript,Python和Kotlin。

EA Async 是一个库,它向 Java 添加了类似 async/await 的功能。该库抽象化了使用ComppletableFutures的复杂性。

但是,为什么async/await既没有被添加到Java SE中,也没有计划在将来添加它呢?


答案 1

简短的回答是,Java的设计者试图消除对异步方法的需求,而不是促进它们的使用。

根据Ron Pressler的谈话,使用ComppletableFuture进行异步编程会导致三个主要问题。

  1. 无法分支或循环访问异步方法调用的结果
  2. 堆栈跟踪不能用于识别错误源,分析变得不可能
  3. 它是病毒式的:所有执行异步调用的方法也必须是异步的,即同步和异步世界不能混合

虽然 async/await 解决了第一个问题,但它只能部分解决第二个问题,而根本不能解决第三个问题(例如,C# 中执行 await 的所有方法都必须标记为 async)。

但是为什么需要异步编程呢?只是为了防止线程阻塞,因为线程很昂贵。因此,在项目中,Loom Java的设计人员不是在Java中引入异步/等待,而是在虚拟线程(又名纤维/轻量级线程)上工作,这将旨在显着降低线程的成本,从而消除异步编程的需要。这将使上述所有三个问题也过时。


答案 2

迟到总比不到好!!!Java在试图提出可以并行执行的更轻量级的执行单元方面晚了10多年。顺便说一句,Project loom还旨在公开Java中的“分隔延续”,我相信这只不过是C#的旧的“yield”关键字(再次晚了近20年!!)

Java确实认识到需要解决由asyn await(或者实际上是C#中的任务)解决的更大问题,这是一个伟大的想法。Async Await更像是一种语法糖。非常显着的改进,但仍然不是解决操作系统映射线程比预期重的实际问题的必要条件)。

在这里查看项目提案:https://cr.openjdk.java.net/~rpressler/loom/Loom-Proposal.html 并导航到最后一节“其他方法”。你会看到为什么Java不想引入async/await。

话虽如此,我并不真正同意所提供的推理。无论是在这个建议中,还是在斯蒂芬的回答中。

首先,让我们诊断斯蒂芬的答案

  1. async await 解决了此处提到的第 1 点。(斯蒂芬也承认了答案)
  2. 对于框架和工具来说,这肯定是额外的工作,但对于程序员来说,这根本不是额外的工作。即使使用异步 await,.Net 调试器在这方面也相当不错。
  3. 我只部分同意这一点。异步等待的全部目的是将异步世界与同步构造优雅地混合在一起。但是,是的,您要么需要将调用方声明为异步,要么直接在调用方例程中处理 Task。但是,项目织机也不会以有意义的方式解决它。为了充分利用轻量级虚拟线程,即使是调用方例程也必须在虚拟线程上执行。否则有什么好处?您最终会阻塞操作系统支持的线程!!!因此,即使是虚拟线程也需要在代码中“病毒式”传播。相反,在Java中更容易不注意到您正在调用的例程是异步的,并且会阻塞调用线程(如果调用例程本身不在虚拟线程上执行,这将引起关注)。C# 中的 Async 关键字使意图非常清晰,并强制您做出决定(如果需要,也可以通过请求 Task.Result 来阻止 C# 中的 Async 关键字。大多数情况下,调用例程本身也很容易异步)。

Stephan说,需要异步编程来防止阻塞(OS)线程,因为(OS)线程很昂贵,这是正确的。这正是需要虚拟线程(或C#任务)的全部原因。您应该能够在不失眠的情况下“阻止”这些任务。为了不失去睡眠,要么调用例程本身应该是一个任务,要么阻塞应该在非阻塞IO上,框架足够智能,在这种情况下不会阻塞调用线程(延续的力量)。

C#支持这一点,而提议的Java功能旨在支持这一点。根据提出的Java api,在虚拟线程上的阻塞将需要在Java中调用vThread.join()方法。它如何真正比调用 await workDoneByVThread() 更有益?

现在让我们来看看项目织机提案的推理

延续和纤程主导 async/await,因为 async/await 很容易通过延续实现(事实上,它可以使用一种称为无堆栈延续的弱分隔延续形式来实现,这些延续不会捕获整个调用堆栈,而只能捕获单个子例程的本地上下文),但反之亦然。

我不简单地理解这句话。如果有人这样做,请在评论中告诉我。

对我来说,async/await是使用延续来实现的,就堆栈跟踪而言,由于光纤/虚拟线程/任务位于虚拟机中,因此必须能够管理该方面。事实上,.net工具确实可以管理这一点。

虽然async/await使代码更简单,并使其看起来像正常的顺序代码,但与异步代码一样,它仍然需要对现有代码进行重大更改,在库中提供显式支持,并且不能与同步代码很好地互操作

我已经介绍了这一点。不对现有代码进行重大更改并且库中没有显式支持实际上意味着不能有效地使用此功能。除非Java的目标是将所有线程透明地转换为虚拟线程,否则它不能也不这样做,否则这句话对我来说没有意义。

作为一个核心思想,我发现Java虚拟线程和C#任务之间没有真正的区别。项目织机还以工作窃取调度程序为目标,作为默认值,与.Net默认使用的调度程序相同(https://docs.microsoft.com/en-us/dotnet/api/system.threading.tasks.taskscheduler?view=net-5.0,滚动到最后一个备注部分)。似乎唯一的争论是关于应该采用什么语法来使用这些语法。

采用 C#

  1. 与现有线程相比,一个不同的类和接口
  2. 非常有用的语法糖,用于将异步与同步相结合

Java的目标是:

  1. 与 Java Thread 相同的熟悉界面
  2. 除了对 ExecutorService 的资源试用支持之外,没有其他特殊构造,因此可以自动等待已提交任务/虚拟线程的结果(从而阻止调用线程,虚拟/非虚拟)。

恕我直言,Java的选择比C#更糟糕。拥有单独的接口和类实际上非常清楚地表明行为有很大不同。当程序员没有意识到她现在正在处理不同的东西时,或者当库实现更改以利用新构造但最终阻止调用(非虚拟)线程时,保留相同的旧接口可能会导致微妙的错误。

此外,没有特殊的语言语法意味着阅读异步代码将仍然难以理解和推理(我不知道为什么Java认为程序员爱上了Java的Thread语法,他们会很高兴知道,他们将使用可爱的Thread类,而不是编写同步代码)

哎呀,甚至Javascript现在也有异步等待(具有其所有的“单线程性”)。


推荐