为什么Java没有async/await?
Why does Java have no async/await?
使用async/await可以以命令式风格编写异步函数。这可以极大地方便异步编程。在 C# 中首次引入后,它被许多语言采用,例如 JavaScript、Python 和 Kotlin。
EA Async 是一个向 Java 添加 async/await 功能的库。该库抽象出使用 CompletableFutures 的复杂性。
但为什么 async/await 既没有添加到 Java SE,也没有计划在将来添加它?
简短的回答是 Java 的设计者试图消除对异步方法的需求,而不是促进它们的使用。
根据 Ron Pressler 的说法,talk 使用 CompletableFuture 的异步编程会导致三个主要问题。
- 不可能对异步方法调用的结果进行分支或循环
- stacktraces 不能用于识别错误源,分析变得不可能
- 它是病毒式传播:所有进行异步调用的方法也必须是异步的,即同步和异步世界不要混合
虽然 async/await 解决了第一个问题,但它只能部分解决第二个问题,根本没有解决第三个问题(例如,C# 中的所有方法都在执行 await 必须标记为 async).
但是为什么需要异步编程呢?只是为了防止线程的阻塞,因为线程是昂贵的。因此,在 Loom Java 项目中,设计人员没有在 Java 中引入 async/await,而是致力于虚拟线程(又名 fibers/lightweight 线程),旨在显着降低线程的成本和从而消除了异步编程的需要。这将使上述所有三个问题也都已过时。
迟到总比不到好!!!
Java 在尝试提出可以并行执行的更轻量级执行单元方面晚了 10 年以上。作为旁注,Project loom 还打算在 Java 'delimited continuation' 中公开,我认为这只不过是 C# 的旧 'yield' 关键字(又晚了将近 20 年!!)
Java 确实认识到需要解决由 asyn await 解决的更大问题(或者实际上是 C# 中的任务,这是一个重要的想法。Async Await 更多的是一种语法糖。非常显着的改进,但仍然不是解决 OS 映射线程比预期重的实际问题的必要条件。
在这里查看项目 loom 的提案:https://cr.openjdk.java.net/~rpressler/loom/Loom-Proposal.html
并导航到最后一部分 'Other Approaches'。你会明白为什么 Java 不想引入 async/await.
话虽如此,我不太同意所提供的推理。无论是在这个提议中还是在斯蒂芬的回答中。
首先让我们来诊断一下斯蒂芬的回答
- async await 解决了那里提到的第 1 点。 (斯蒂芬在答案的下方也承认了这一点)
- 对于框架和工具而言,这肯定是额外的工作,但对于程序员而言则完全不是。即使有 async await,.Net 调试器在这方面也很不错。
- 这点我只同意一部分。 async await 的全部目的是将异步世界与同步结构优雅地混合在一起。但是,是的,您要么需要将调用者也声明为异步的,要么直接在调用者例程中处理 Task 。然而,loom 项目也不会以有意义的方式解决它。为了充分受益于轻量级虚拟线程,即使是调用程序也必须在虚拟线程上执行。不然有什么好处?您最终会阻塞 OS 支持的线程!!!因此,即使是虚拟线程也需要在代码中为 'viral'。相反,在 Java 中更容易注意到您正在调用的例程是异步的,并且会阻塞调用线程(如果调用例程本身不在虚拟线程上执行,这将是令人担忧的)。 C# 中的 Async 关键字使意图非常明确并迫使您做出决定(如果您愿意,在 C# 中也可以通过请求 Task.Result 来阻止。大多数情况下,调用例程本身可以很容易地异步).
Stephan 说得对,他说需要异步编程来防止阻塞 (OS) 个线程,因为 (OS) 个线程很昂贵。这正是需要虚拟线程(或 C# 任务)的全部原因。您应该能够 'block' 完成这些任务而不会失眠。当然,为了不失眠,调用例程本身应该是一个任务,或者阻塞应该在非阻塞 IO 上,框架足够智能,在这种情况下不会阻塞调用线程(继续的力量)。
C# 支持此功能并提出 Java 功能旨在支持此功能。
根据提议的 Java api,在虚拟线程上阻塞将需要调用 Java 中的 vThread.join() 方法。
它真的比调用 await workDoneByVThread() 更有益吗?
现在让我们来看看项目机子提案推理
Continuations and fibers dominate async/await in the sense that async/await is easily implemented with continuations (in fact, it can be implemented with a weak form of delimited continuations known as stackless continuations, that don't capture an entire call-stack but only the local context of a single subroutine), but not vice-versa
我不是简单地理解这个说法。如果有人这样做,请在评论中告诉我。
对我来说,async/await 是使用延续实现的,就堆栈跟踪而言,由于 fibres/virtual threads/tasks 在虚拟机中,因此必须可以管理那个方面。事实上 .net 工具确实可以做到这一点。
While async/await makes code simpler and gives it the appearance of normal, sequential code, like asynchronous code it still requires significant changes to existing code, explicit support in libraries, and does not interoperate well with synchronous code
我已经介绍过了。不对现有代码进行重大更改并且在库中没有明确支持实际上意味着不能有效地使用此功能。直到并且除非 Java 的目标是透明地将所有线程转换为虚拟线程,它不能也不是,这个声明对我来说没有意义。
作为一个核心思想,我发现 Java 虚拟线程和 C# 任务之间没有真正的区别。就这一点而言,Loom 项目也将工作窃取调度程序作为默认目标,与 .Net 默认使用的调度程序相同(https://docs.microsoft.com/en-us/dotnet/api/system.threading.tasks.taskscheduler?view=net-5.0,滚动到最后的备注部分)。
似乎唯一的争论是应该采用什么语法来使用这些。
采用C#
- 与现有线程
相比,一个独特的class和接口
- 非常有用的语法糖,用于将异步与同步结合起来
Java 的目标是:
- 与 Java Thread
相同的熟悉界面
- 除了对 ExecutorService 的 try-with-resources 支持之外,没有特殊的构造,以便可以自动等待提交的 tasks/virtual 线程的结果(从而阻塞调用线程,virtual/non-virtual)。
恕我直言,Java 的选择比 C# 的选择更糟糕。有一个单独的接口和 class 实际上很清楚行为有很大不同。当程序员没有意识到她现在正在处理不同的东西时,或者当库实现发生变化以利用新结构但最终阻塞调用(非虚拟)线程时,保留相同的旧接口可能会导致细微的错误。
也没有特殊的语言语法意味着阅读异步代码仍然难以理解和推理(我不知道为什么 Java 认为程序员喜欢 Java 的线程语法他们会很高兴知道他们将使用可爱的 Thread 而不是编写看起来同步的代码 class)
哎呀,甚至 Java 脚本现在也有异步等待(及其所有 'single-threadedness')。
我发布了一个新项目 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除了编码之外,还大大提升了异步代码的调试体验。
调试的时候和调试普通代码一样,可以在monitorwindow中看到所有的变量。我会尽力解决Stephan提到的第2点。
对于第3点,我觉得问题不大。 Async/Await流行c#和es就算不满意
使用async/await可以以命令式风格编写异步函数。这可以极大地方便异步编程。在 C# 中首次引入后,它被许多语言采用,例如 JavaScript、Python 和 Kotlin。
EA Async 是一个向 Java 添加 async/await 功能的库。该库抽象出使用 CompletableFutures 的复杂性。
但为什么 async/await 既没有添加到 Java SE,也没有计划在将来添加它?
简短的回答是 Java 的设计者试图消除对异步方法的需求,而不是促进它们的使用。
根据 Ron Pressler 的说法,talk 使用 CompletableFuture 的异步编程会导致三个主要问题。
- 不可能对异步方法调用的结果进行分支或循环
- stacktraces 不能用于识别错误源,分析变得不可能
- 它是病毒式传播:所有进行异步调用的方法也必须是异步的,即同步和异步世界不要混合
虽然 async/await 解决了第一个问题,但它只能部分解决第二个问题,根本没有解决第三个问题(例如,C# 中的所有方法都在执行 await 必须标记为 async).
但是为什么需要异步编程呢?只是为了防止线程的阻塞,因为线程是昂贵的。因此,在 Loom Java 项目中,设计人员没有在 Java 中引入 async/await,而是致力于虚拟线程(又名 fibers/lightweight 线程),旨在显着降低线程的成本和从而消除了异步编程的需要。这将使上述所有三个问题也都已过时。
迟到总比不到好!!! Java 在尝试提出可以并行执行的更轻量级执行单元方面晚了 10 年以上。作为旁注,Project loom 还打算在 Java 'delimited continuation' 中公开,我认为这只不过是 C# 的旧 'yield' 关键字(又晚了将近 20 年!!)
Java 确实认识到需要解决由 asyn await 解决的更大问题(或者实际上是 C# 中的任务,这是一个重要的想法。Async Await 更多的是一种语法糖。非常显着的改进,但仍然不是解决 OS 映射线程比预期重的实际问题的必要条件。
在这里查看项目 loom 的提案:https://cr.openjdk.java.net/~rpressler/loom/Loom-Proposal.html 并导航到最后一部分 'Other Approaches'。你会明白为什么 Java 不想引入 async/await.
话虽如此,我不太同意所提供的推理。无论是在这个提议中还是在斯蒂芬的回答中。
首先让我们来诊断一下斯蒂芬的回答
- async await 解决了那里提到的第 1 点。 (斯蒂芬在答案的下方也承认了这一点)
- 对于框架和工具而言,这肯定是额外的工作,但对于程序员而言则完全不是。即使有 async await,.Net 调试器在这方面也很不错。
- 这点我只同意一部分。 async await 的全部目的是将异步世界与同步结构优雅地混合在一起。但是,是的,您要么需要将调用者也声明为异步的,要么直接在调用者例程中处理 Task 。然而,loom 项目也不会以有意义的方式解决它。为了充分受益于轻量级虚拟线程,即使是调用程序也必须在虚拟线程上执行。不然有什么好处?您最终会阻塞 OS 支持的线程!!!因此,即使是虚拟线程也需要在代码中为 'viral'。相反,在 Java 中更容易注意到您正在调用的例程是异步的,并且会阻塞调用线程(如果调用例程本身不在虚拟线程上执行,这将是令人担忧的)。 C# 中的 Async 关键字使意图非常明确并迫使您做出决定(如果您愿意,在 C# 中也可以通过请求 Task.Result 来阻止。大多数情况下,调用例程本身可以很容易地异步).
Stephan 说得对,他说需要异步编程来防止阻塞 (OS) 个线程,因为 (OS) 个线程很昂贵。这正是需要虚拟线程(或 C# 任务)的全部原因。您应该能够 'block' 完成这些任务而不会失眠。当然,为了不失眠,调用例程本身应该是一个任务,或者阻塞应该在非阻塞 IO 上,框架足够智能,在这种情况下不会阻塞调用线程(继续的力量)。
C# 支持此功能并提出 Java 功能旨在支持此功能。 根据提议的 Java api,在虚拟线程上阻塞将需要调用 Java 中的 vThread.join() 方法。 它真的比调用 await workDoneByVThread() 更有益吗?
现在让我们来看看项目机子提案推理
Continuations and fibers dominate async/await in the sense that async/await is easily implemented with continuations (in fact, it can be implemented with a weak form of delimited continuations known as stackless continuations, that don't capture an entire call-stack but only the local context of a single subroutine), but not vice-versa
我不是简单地理解这个说法。如果有人这样做,请在评论中告诉我。
对我来说,async/await 是使用延续实现的,就堆栈跟踪而言,由于 fibres/virtual threads/tasks 在虚拟机中,因此必须可以管理那个方面。事实上 .net 工具确实可以做到这一点。
While async/await makes code simpler and gives it the appearance of normal, sequential code, like asynchronous code it still requires significant changes to existing code, explicit support in libraries, and does not interoperate well with synchronous code
我已经介绍过了。不对现有代码进行重大更改并且在库中没有明确支持实际上意味着不能有效地使用此功能。直到并且除非 Java 的目标是透明地将所有线程转换为虚拟线程,它不能也不是,这个声明对我来说没有意义。
作为一个核心思想,我发现 Java 虚拟线程和 C# 任务之间没有真正的区别。就这一点而言,Loom 项目也将工作窃取调度程序作为默认目标,与 .Net 默认使用的调度程序相同(https://docs.microsoft.com/en-us/dotnet/api/system.threading.tasks.taskscheduler?view=net-5.0,滚动到最后的备注部分)。 似乎唯一的争论是应该采用什么语法来使用这些。
采用C#
- 与现有线程 相比,一个独特的class和接口
- 非常有用的语法糖,用于将异步与同步结合起来
Java 的目标是:
- 与 Java Thread 相同的熟悉界面
- 除了对 ExecutorService 的 try-with-resources 支持之外,没有特殊的构造,以便可以自动等待提交的 tasks/virtual 线程的结果(从而阻塞调用线程,virtual/non-virtual)。
恕我直言,Java 的选择比 C# 的选择更糟糕。有一个单独的接口和 class 实际上很清楚行为有很大不同。当程序员没有意识到她现在正在处理不同的东西时,或者当库实现发生变化以利用新结构但最终阻塞调用(非虚拟)线程时,保留相同的旧接口可能会导致细微的错误。
也没有特殊的语言语法意味着阅读异步代码仍然难以理解和推理(我不知道为什么 Java 认为程序员喜欢 Java 的线程语法他们会很高兴知道他们将使用可爱的 Thread 而不是编写看起来同步的代码 class)
哎呀,甚至 Java 脚本现在也有异步等待(及其所有 'single-threadedness')。
我发布了一个新项目 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除了编码之外,还大大提升了异步代码的调试体验。 调试的时候和调试普通代码一样,可以在monitorwindow中看到所有的变量。我会尽力解决Stephan提到的第2点。
对于第3点,我觉得问题不大。 Async/Await流行c#和es就算不满意