主题
44 · 异步模式与 Java 25(Asynchronous Mode and Java 25)
异步配置定义在 AsyncConfiguration.kt 中。
线程模型(Threading Model)
Embabel 为所有 Agent 操作使用专用执行器,并采用基于继承的线程模型。
升级注意 —— 在此版本之前,Embabel 默认设置
spring.threads.virtual.enabled=true, 强制所有应用使用虚拟线程。该行为已被移除,以避免干扰宿主应用的线程配置。 现在 Embabel 从spring.threads.virtual.enabled继承线程模型(未设置时默认为平台线程)。 要启用虚拟线程,请在application.properties中显式设置spring.threads.virtual.enabled=true。
配置属性
两个正交的属性控制 Embabel 的线程行为:
- 线程模型继承:
embabel.agent.platform.threading.overridefalse(默认):从spring.threads.virtual.enabled继承线程模型;true:翻转线程模型(virtual → platform,或 platform → virtual)。
- 执行器共享:
embabel.agent.platform.threading.sharedfalse(默认):为 Embabel 操作创建隔离的执行器;true:当线程模型一致时,共享应用的applicationTaskExecutorbean。
在 agent-platform.properties 中配置:
properties
# 翻转继承来的线程模型(virtual→platform,platform→virtual)
embabel.agent.platform.threading.override=false
# 当应用与 Embabel 使用相同线程模型时,共享应用的执行器
embabel.agent.platform.threading.shared=false行为矩阵(Behavior Matrix)
| 应用线程 | override | shared | Embabel 线程模型 | 执行器类型 |
|---|---|---|---|---|
| Platform | false | false | Platform(继承) | 隔离的平台执行器(默认) |
| Platform | false | true | Platform(继承) | 共享应用的平台执行器 |
| Platform | true | false | Virtual(翻转) | 隔离的虚拟执行器 |
| Platform | true | true | Virtual(翻转) | 隔离的虚拟执行器(模型不同) |
| Virtual | false | false | Virtual(继承) | 隔离的虚拟执行器 |
| Virtual | false | true | Virtual(继承) | 共享应用的虚拟执行器 |
| Virtual | true | false | Platform(翻转) | 隔离的平台执行器 |
| Virtual | true | true | Platform(翻转) | 隔离的平台执行器(模型不同) |
关键点:shared=true 只有在应用与 Embabel 使用相同线程模型时(platform/platform 或 virtual/virtual)才启用执行器共享; 线程模型不一致时,Embabel 总是创建隔离的执行器。
注意:虚拟线程需要 Java 21+。在 Java 17–20 上,即使请求了虚拟线程,Embabel 也会自动回退到平台线程。
框架在 Agent 调用、Agent 动作、工具循环、Shell 命令等场景中一致地使用 Asyncer 抽象。
Java 25 的影响(Java 25 Implications)
在 Java 25 之前,JVM 读取容器 CPU 限额时不精确 —— 它常常看到宿主机的全部 CPU 数,而不是容器的 cgroup 配额。 Java 25 修正了这一点,会准确遵循 cgroup CPU 限额(例如 Kubernetes 的 resources.limits.cpu)。 相关 OpenJDK 工单:JDK-8362881。
当在容器(Docker / Kubernetes)里用 Java 25 运行 Embabel 时,所有 Agent 动作会串行执行而不是并行。 原因正是 Java 25 更准确的 cgroup CPU 检测:在受 CPU 限制的容器里它报告 availableProcessors() = 1。 由于 ForkJoinPool.commonPool 会据此决定并行度,实际并行度降到 1,于是所有并发 Agent 操作被串行化。
| JVM 版本 | availableProcessors() | Common pool 并行度 | 行为 |
|---|---|---|---|
| 25 之前 | 宿主机 CPU 数(例如 8) | 7 | 动作并行执行 |
| 25 | 容器 CPU 限额(例如 1) | 1(最小值) | 动作串行执行 |
另外请注意:现代(1.3 之后)Kotlin 协程在 Dispatchers.Default 与 Dispatchers.IO 上的实现, 背后是 Kotlin 自己的协程调度器,而不是 Java 的 ForkJoinPool.commonPool()。
为什么 Embabel 是安全的
- Asyncer 抽象 —— 所有并行执行都走
ExecutorAsyncer; - 专用执行器 —— 使用专用执行器(虚拟线程或无界平台线程),不是
ForkJoinPool; - 无界执行 —— 配置为无界,以支持并行动作执行;
- 协程默认值 —— 生产代码使用
Dispatchers.IO,而不是Dispatchers.Default。
如果用户报告 Java 25 上的串行化问题,建议检查:
- 自定义代码里使用了
CompletableFuture.supplyAsync()而未指定执行器; - 自定义代码使用了
Dispatchers.Default(Kotlin 协程); - Spring 配置有误 ——
applicationTaskExecutor未正确配置; - 依赖
ForkJoinPool.commonPool的第三方库。
变通方案(如确需)
尽管 Embabel 核心是安全的,但若你有受影响的自定义代码,可以采取以下设置:
方案 1:设置容器 CPU 限额
yaml
# Kubernetes
resources:
limits:
cpu: "4"
requests:
cpu: "2"方案 2:覆盖 ForkJoinPool 并行度
bash
java -Djava.util.concurrent.ForkJoinPool.common.parallelism=4 -jar agent.jar