为啥 Java 没有 async/await?
Posted
技术标签:
【中文标题】为啥 Java 没有 async/await?【英文标题】:Why does Java have no async/await?为什么 Java 没有 async/await? 【发布时间】:2020-01-24 06:46:52 【问题描述】:使用 async/await 可以以命令式风格编写异步函数。这可以极大地方便异步编程。它在 C# 中首次引入后,被 javascript、Python 和 Kotlin 等多种语言采用。
EA Async 是一个向 Java 添加类似 async/await 功能的库。该库抽象出使用 CompletableFutures 的复杂性。
但是为什么 async/await 既没有被添加到 Java SE 中,也没有计划在未来添加呢?
【问题讨论】:
【参考方案1】:简短的回答是,Java 的设计者试图消除对异步方法的需求,而不是促进它们的使用。
根据 Ron Pressler 的 talk 使用 CompletableFuture 进行异步编程会导致三个主要问题。
-
无法对异步方法调用的结果进行分支或循环
堆栈跟踪不能用于识别错误源,无法进行分析
它是病毒式的:所有进行异步调用的方法也必须是异步的,即同步和异步世界不能混合
虽然 async/await 解决了第一个问题,但它只能部分解决第二个问题,根本没有解决第三个问题(例如,C# 中执行 await 的所有方法都必须标记为 异步)。
但是为什么需要异步编程呢?只是为了防止线程阻塞,因为线程很昂贵。因此,在 Loom 项目中,Java 设计人员没有在 Java 中引入 async/await,而是致力于虚拟线程(又名纤程/轻量级线程),旨在显着降低线程成本,从而消除对异步编程的需求。这将使上述所有三个问题也都过时了。
【讨论】:
Fibers 听起来像线程,但没有让程序员做那么多。根据这个描述,这似乎是净损失。 那次谈话非常自以为是。对于 1),async/await 使其成为非问题;没有它们,您将使用TaskCompletionSource<T>
(例如,没有 lambda 的 CompletableFuture<T>
),在内部处理条件和循环并根据需要完成此对象。对于 2),运行时关联堆栈跟踪并且 IDE 可以理解它,所以问题不大;即使没有相关性,您仍然会看到 CPU 瓶颈。对于 3),如果您没有一直使用异步,那么您在某处阻塞,因此病毒式传播在任何方面都与异步有关,而不是与异步/等待有关。
至于在 C# 中使用async
标记方法,这主要与识别上下文await
关键字有关。编译的方法没有任何异步标志。至于纤程,它们需要来自堆栈中的每个本机库或托管运行时的支持。在某种程度上,纤程支持也是“病毒式”的,但是以负面的方式:几乎没有人关心他们的库或运行时是否不适用于纤程。
您忘记了异步/等待背后的主要原因:代码可读性。在复杂的应用程序中,在没有 async/await 的情况下会发生大量异步调用(如 http 后端调用),您最终会得到这种带有调用链的意大利面条式代码。它很难阅读、调试和理解。使用 async/await 模式,你最终会得到一个漂亮的类似同步的代码
C# 有任务,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。
话虽如此,我并不真正同意所提供的推理。在这个提议和斯蒂芬的回答中都没有。
首先让我们诊断一下斯蒂芬的答案
-
async await 解决了上面提到的第 1 点。 (Stephan 也进一步承认了这一点)
这对框架和工具来说肯定是额外的工作,但对程序员来说根本不是。即使使用异步等待,.Net 调试器在这方面也相当出色。
我只是部分同意。 async await 的全部目的是优雅地混合异步世界和同步结构。但是,是的,您要么需要将调用者也声明为异步,要么直接在调用者例程中处理 Task。但是,project loom 也不会以有意义的方式解决它。为了充分受益于轻量级虚拟线程,即使是调用例程也必须在虚拟线程上执行。不然有什么好处?你最终会阻塞一个操作系统支持的线程!!!因此,即使是虚拟线程也需要在代码中具有“病毒性”。相反,在 Java 中更容易注意到您正在调用的例程是异步的并且会阻塞调用线程(如果调用例程本身不在虚拟线程上执行,这将是令人担忧的)。 C# 中的 Async 关键字使意图非常明确并迫使您做出决定(如果您想通过询问 Task.Result 来阻止,C# 中也可以阻止。大多数情况下,调用例程本身也可以很容易地异步)。
Stephan 说需要异步编程来防止 (OS) 线程阻塞是正确的,因为 (OS) 线程很昂贵。这正是需要虚拟线程(或 C# 任务)的全部原因。您应该能够在不失眠的情况下“阻止”这些任务。为了不失去睡眠,调用例程本身应该是一个任务,或者阻塞应该在非阻塞 IO 上,框架足够聪明,不会在这种情况下阻塞调用线程(继续的力量)。
C# 支持这一点,并且提议的 Java 特性旨在支持这一点。 根据提议的 Java api,阻塞虚拟线程将需要在 Java 中调用 vThread.join() 方法。 真的比调用 await workDoneByVThread() 更有益吗?
现在让我们看看项目织机提案推理
在 async/await 很容易用 continuation 实现的意义上,Continuations 和 Fiber 支配 async/await(实际上,它可以用一种称为 stackless continuation 的弱分隔延续形式来实现,它不会捕获整个调用-stack 但仅是单个子程序的本地上下文),反之则不行
我不明白这个说法。如果有人这样做,请在 cmets 中告诉我。
对我来说,异步/等待是使用延续实现的,就堆栈跟踪而言,由于纤程/虚拟线程/任务位于虚拟机中,因此必须可以管理该方面。事实上 .net 工具确实可以做到这一点。
虽然 async/await 使代码更简单,并使它看起来像正常的顺序代码,就像异步代码一样,但它仍然需要对现有代码进行重大更改,库中的明确支持,并且不能与同步代码很好地互操作
我已经介绍过了。不对现有代码进行重大更改并且在库中没有明确支持实际上意味着无法有效地使用此功能。除非 Java 的目标是透明地将所有线程转换为虚拟线程,但它不能也不是,这句话对我来说没有意义。
作为一个核心思想,我发现 Java 虚拟线程和 C# 任务之间没有真正的区别。以至于项目 loom 也将工作窃取调度程序作为默认目标,与 .Net 默认使用的调度程序相同(https://docs.microsoft.com/en-us/dotnet/api/system.threading.tasks.taskscheduler?view=net-5.0,滚动到最后的备注部分)。 似乎唯一的争论是应该采用什么语法来消费这些。
采用C#
-
与现有线程相比,一个独特的类和接口
非常有用的语法糖,用于将异步与同步结合起来
Java 的目标是:
-
同样熟悉的 Java Thread 界面
除了对 ExecutorService 的 try-with-resources 支持之外没有任何特殊结构,因此可以自动等待提交的任务/虚拟线程的结果(从而阻塞调用线程,虚拟/非虚拟)。
恕我直言,Java 的选择比 C# 更糟糕。拥有一个单独的接口和类实际上非常清楚地表明行为有很大不同。当程序员没有意识到她现在正在处理不同的东西时,或者当库实现更改以利用新结构但最终阻塞调用(非虚拟)线程时,保留相同的旧接口可能会导致细微的错误。
此外,没有特殊的语言语法意味着阅读异步代码仍然难以理解和推理(我不知道为什么 Java 认为程序员爱上了 Java 的线程语法,他们会很高兴知道这一点而不是编写同步寻找代码,他们将使用可爱的 Thread 类)
哎呀,现在甚至 Javascript 也有异步等待(具有所有的“单线程”)。
【讨论】:
【参考方案3】:我发布了一个新项目JAsync 在 java 中实现异步等待方式,它使用 Reactor 作为其低级框架。它处于阿尔法阶段。我需要更多建议和测试用例。 该项目使开发人员的异步编程体验尽可能接近通常的同步编程,包括编码和调试。 我认为我的项目解决了 Stephan 提到的第 1 点。
这是一个例子:
@RestController
@RequestMapping("/employees")
public class MyRestController
@Inject
private EmployeeRepository employeeRepository;
@Inject
private SalaryRepository salaryRepository;
// The standard JAsync async method must be annotated with the Async annotation, and return a JPromise object.
@Async()
private JPromise<Double> _getEmployeeTotalSalaryByDepartment(String department)
double money = 0.0;
// A Mono object can be transformed to the JPromise object. So we get a Mono object first.
Mono<List<Employee>> empsMono = employeeRepository.findEmployeeByDepartment(department);
// Transformed the Mono object to the JPromise object.
JPromise<List<Employee>> empsPromise = Promises.from(empsMono);
// Use await just like es and c# to get the value of the JPromise without blocking the current thread.
for (Employee employee : empsPromise.await())
// The method findSalaryByEmployee also return a Mono object. We transform it to the JPromise just like above. And then await to get the result.
Salary salary = Promises.from(salaryRepository.findSalaryByEmployee(employee.id)).await();
money += salary.total;
// The async method must return a JPromise object, so we use just method to wrap the result to a JPromise.
return JAsync.just(money);
// This is a normal webflux method.
@GetMapping("/department/salary")
public Mono<Double> getEmployeeTotalSalaryByDepartment(@PathVariable String department)
// Use unwrap method to transform the JPromise object back to the Mono object.
return _getEmployeeTotalSalaryByDepartment(department).unwrap(Mono.class);
除了编码之外,JAsync 还大大提升了异步代码的调试体验。 调试时,您可以像调试正常代码时一样在监视窗口中看到所有变量。我会尽力解决Stephan提到的第2点。
对于第3点,我认为这不是一个大问题。 Async/Await 在 c# 和 es 中很流行,即使对它不满意。
【讨论】:
以上是关于为啥 Java 没有 async/await?的主要内容,如果未能解决你的问题,请参考以下文章
为啥 try .. catch() 不能与 async/await 函数一起使用?
为啥 Typescript 认为 async/await 返回包装在承诺中的值?