ThreadForge 源码解读三:从任务执行到并发编排,ScopeJoiner 是怎么工作的?

ThreadForge 源码解读三:从任务执行到并发编排,ScopeJoiner 是怎么工作的?

前两篇讲了 ThreadScope 的边界和 Task 的执行链路。这一篇继续深入,我们来看高阶并发编排。

当业务需要从多个并发请求中获取第一个成功结果 、根据多数请求的结果进行决策 ,或者通过发送备用请求来降低长尾延迟时,代码应该如何实现?

ThreadForge 给出的答案是 ScopeJoinerJoinStrategy

1. 为什么需要 JoinStrategy

普通并发就是:把多个任务跑起来,等它们结束。

但真实业务里,经常不需要等全部完成。

first success

比如下面这些场景:

  • 缓存和多个下游副本同时请求,只要第一个成功:先查缓存,再查副本;
  • 主服务和备服务并发;
  • 多数据源抢首个可用结果。

有一个成功就要及时结束。

quorum

  • 多个副本并发,成功数达到阈值即可;
  • 5 个副本拿到 3 个成功就返回,多个节点投票达到法定票数就继续;

这类场景关心的不是全量,是数量足够后就结束。

hedged request

先发主请求,慢了再补发备用请求,目标是压长尾。

这类逻辑如果让业务自己写,常见结果是一堆 CompletableFuture.anyOf(...) ,再加上手动取消剩余任务、手动补延迟启动、手动处理已经不可能成功的分支。

ThreadForge 把这些模式统一收敛成了 JoinStrategy

2. JoinStrategy:只描述结果目标

src/main/java/io/threadforge/JoinStrategy.java

java 复制代码
enum Mode {
    FIRST_SUCCESS,
    QUORUM,
    HEDGED
}

三个工厂方法:

java 复制代码
public static <T> JoinStrategy<T, T> firstSuccess()
public static <T> JoinStrategy<T, List<T>> quorum(int requiredSuccesses)
public static <T> JoinStrategy<T, T> hedged(Duration hedgeDelay)
  • firstSuccess() --- 拿第一个成功值
  • quorum(n) --- 拿到 n 个成功值
  • hedged(delay) --- 主请求先跑,延迟后放出备援

JoinStrategy 只定义任务的合并目标,具体的启动、结果汇总和任务取消由 ScopeJoiner 统一处理。

classDiagram class JoinStrategy { -Mode mode -int requiredSuccesses -Duration hedgeDelay +firstSuccess() +quorum(int) +hedged(Duration) +isFirstSuccess() +isQuorum() +isHedged() } class ScopeJoiner { -ThreadScope scope +join(strategy, callables) +firstSuccess(callables) +quorum(requiredSuccesses, callables) +hedged(delay, callables) } class JoinState { -CompletableFuture result -List~Task~ tasks -List~ScheduledTask~ launchers -List~Throwable~ failures -List~T~ successes -int completed +launch(callable) +scheduleHedged(callable, delay) +handleCompletion(task, value, throwable) +cancelPending(winner) } ScopeJoiner --> JoinStrategy ScopeJoiner --> JoinState ScopeJoiner --> ThreadScope JoinState --> Task JoinState --> ScheduledTask

三层分工:JoinStrategy 定义模式,ScopeJoiner 对外入口,JoinState 内部推进状态。

3. ScopeJoiner.join:总流程

入口:

java 复制代码
public <T, R> R join(JoinStrategy<T, R> strategy, Collection<? extends Callable<T>> callables)

简单来讲就是先校验参数并创建 JoinState,然后根据策略启动任务,最后等待并返回执行结果。

关键代码如下:

java 复制代码
JoinState<T, R> state = new JoinState<T, R>(scope, strategy, taskList.size());

if (strategy.isHedged()) {
    state.launch(taskList.get(0));
    for (int i = 1; i < taskList.size(); i++) {
        state.scheduleHedged(taskList.get(i), strategy.hedgeDelay());
    }
} else {
    for (Callable<T> callable : taskList) {
        state.launch(callable);
    }
}

return scope.awaitJoinFuture(state.result());

任务统一通过 scope.submit(...) 提交,并通过 scope.awaitJoinFuture(...) 等待执行结果。

这样一来,整个 join 过程始终由 ThreadScope 管理,任务取消和超时控制仍然遵循底层运行时的规则。

flowchart TD A[ScopeJoiner.join] --> B[校验参数] B --> C[创建 JoinState] C --> D{是否 Hedged?} D -->|否| E[立即 launch 所有 callable] D -->|是| F[立即 launch 第一个 callable] F --> G[其余 callable 延迟 scheduleHedged] E --> H[等待 JoinState.result] G --> H H --> I{结果完成?} I -->|成功| J[返回结果] I -->|失败| K[抛出异常]

4. JoinState:内部状态机

JoinStateScopeJoiner 的私有内部类。

核心字段如下:

java 复制代码
private final CompletableFuture<R> result;
private final List<Task<T>> tasks;
private final List<ScheduledTask> launchers;
private final List<Throwable> failures;
private final List<T> successes;
private final Object monitor;
private int completed;
  • result --- 最终等待的 future
  • tasks --- 已启动的候选任务
  • launchers --- hedged 模式下的延迟启动器
  • failures / successes --- 结果收集
  • monitor --- 保护共享状态
  • completed --- 已完成计数

整个处理过程可以分为四步:先启动所有候选任务,再持续收集任务结果;当结果满足策略要求时,立即作出决策,并取消不再需要的剩余任务。

各种高级并发模式,本质上都是基于这套流程实现的。

5. firstSuccess:第一个成功就结束

当多个任务并发执行时,只要其中一个任务成功,就立即完成整个 join,并取消其他尚未结束的任务。

handleCompletion(...) 会先记录成功结果:

java 复制代码
if (throwable == null) {
    successes.add(value);
    if (!result.isDone()) {
        if (strategy.isFirstSuccess() || strategy.isHedged()) {
            successToComplete = value;
        }
    }
}

离开同步块后,再尝试用该结果完成 result

java 复制代码
if (successToComplete != null) {
    if (result.complete((R) successToComplete)) {
        cancelPending(task);
    }
    return;
}

这里的核心逻辑是:任意任务成功后,都可以尝试完成最终结果;只有第一个成功完成 result 的任务会被保留,其余任务全部取消。

需要注意的是,这种策略比较的是谁先成功,而不是谁先结束。

某个任务即使最先返回,但结果是失败,也不会结束整个 join;只有最先返回成功结果的任务才算胜出。

sequenceDiagram participant User participant Joiner as ScopeJoiner participant Scope as ThreadScope participant T1 as Task A participant T2 as Task B participant T3 as Task C participant State as JoinState User->>Joiner: firstSuccess(A, B, C) Joiner->>Scope: submit(A) Joiner->>Scope: submit(B) Joiner->>Scope: submit(C) T2-->>State: success(valueB) State->>State: result.complete(valueB) State->>T1: cancel() State->>T3: cancel() State-->>User: valueB

6. quorum:成功数够了就结束

当成功任务的数量达到指定阈值后,立即返回前 N 个成功结果:

java 复制代码
scope.joiner().quorum(3, callables);  // 收集到 3 个成功结果后返回

对应的判断逻辑如下:

java 复制代码
else if (strategy.isQuorum() && successes.size() >= strategy.requiredSuccesses()) {
    successToComplete = Collections.unmodifiableList(
        new ArrayList<T>(successes.subList(0, strategy.requiredSuccesses()))
    );
}

系统还会提前判断当前任务是否已经不可能达到目标:

java 复制代码
private boolean cannotSucceed() {
    int target = strategy.isQuorum() ? strategy.requiredSuccesses() : 1;
    int remainingPossible = totalCandidates - completed;
    return successes.size() + remainingPossible < target;
}

系统会结合当前成功数量和剩余候选任务数量,判断是否还有可能达到目标阈值。

如果剩余任务全部成功后仍无法满足要求,就立即结束 join,不再继续等待。

flowchart TD A[任务完成] --> B{是否成功?} B -->|成功| C[successes.add] C --> D{successes >= requiredSuccesses?} D -->|是| E[完成 result] E --> F[取消 pending tasks] D -->|否| G[继续等待] B -->|失败| H[failures.add] H --> I{无法达成 quorum?} I -->|是| J[AggregateException / CancelledException] J --> K[取消 pending tasks] I -->|否| G
sequenceDiagram participant State as JoinState participant A as Task A participant B as Task B participant C as Task C participant D as Task D participant E as Task E Note over State: quorum = 3 A-->>State: success State->>State: successes = 1 B-->>State: failed State->>State: failures = 1 C-->>State: success State->>State: successes = 2 D-->>State: success State->>State: successes = 3 State->>State: result.complete([A,C,D]) State->>E: cancel()

失败时 JoinState.failure() 区分两种场景:有真实失败的返回 AggregateException,纯粹因取消达不到 quorum 的返回 CancelledException

7. hedged request:通过备用请求降低长尾延迟

Hedged Request 的执行方式是:先发送主请求。如果主请求在 hedgeDelay 内没有返回,再启动备用请求,最终采用最先成功的结果。

java 复制代码
if (strategy.isHedged()) {
    state.launch(taskList.get(0));
    for (int i = 1; i < taskList.size(); i++) {
        state.scheduleHedged(taskList.get(i), strategy.hedgeDelay());
    }
}

第一个任务会立即执行,后续任务则通过 scheduleHedged(...) 延迟启动:

java 复制代码
ScheduledTask launcher = scope.schedule(delay, () -> {
    if (result.isDone()) {
        return;
    }
    scope.token().throwIfCancelled();
    launch(callable);
});

延迟时间到达后,系统会先检查最终结果是否已经产生。

如果主请求已经成功,就不再启动备用请求;如果当前 ThreadScope 已被取消,备用请求也不会继续执行。

sequenceDiagram participant User participant Joiner as ScopeJoiner participant Scope as ThreadScope participant Delay as DelayScheduler participant Main as Main Task participant Backup as Backup Task participant State as JoinState User->>Joiner: hedged(50ms, main, backup) Joiner->>Scope: submit(main) Joiner->>Delay: schedule backup launcher after 50ms alt main returns before 50ms Main-->>State: success State->>State: result.complete(mainResult) State->>Delay: cancel backup launcher State-->>User: mainResult else main is slow Delay-->>State: trigger backup launcher State->>Scope: submit(backup) Backup-->>State: success State->>State: result.complete(backupResult) State->>Main: cancel() State-->>User: backupResult end

这种方式不会一开始就同时发送所有请求,而是在主请求响应较慢时再启动备用请求,从而在控制额外资源消耗的同时,降低长尾请求对整体响应时间的影响。

8. cancelPending:取消不再需要的任务

无论采用哪种 join 策略,只要最终结果已经确定,就应立即取消剩余任务。

JoinState.cancelPending(...) 的处理逻辑如下:

java 复制代码
for (ScheduledTask launcher : launchersSnapshot) {
    if (!launcher.isDone()) {
        launcher.cancel();
    }
}

for (Task<T> task : tasksSnapshot) {
    if (task != winner && !task.isDone()) {
        task.cancel();
    }
}

这里需要处理两类对象:

  • 尚未触发的 Hedged Request 延迟启动器;
  • 已经启动,但最终结果不再依赖的任务。

对于获胜任务,代码会通过 task != winner 将其保留;其他仍在运行的任务则直接取消。

flowchart TD A[result 完成] --> B[复制 tasks 快照] A --> C[复制 launchers 快照] C --> D[取消尚未触发的 launcher] B --> E[取消非 winner 且尚未完成的任务] D --> F[结束剩余工作] E --> F

完成 result 只能让调用方提前返回,取消剩余任务才能避免无效计算继续占用线程、连接和其他运行时资源。

9. 所有编排都由 ThreadScope 统一管理

并发编排如果绕开底层任务运行时,通常会形成两套生命周期管理机制:普通任务由底层框架负责取消和超时,高级编排则需要单独维护自己的状态和控制逻辑。

这样不仅实现复杂,也容易出现行为不一致。

ScopeJoiner 没有额外建立一套任务运行机制,而是始终通过 ThreadScope 提供的 API 完成任务提交、延迟调度和结果等待:

java 复制代码
scope.submit(callable);                 // 提交任务
scope.schedule(delay, runnable);        // 延迟执行
scope.awaitJoinFuture(state.result());  // 等待最终结果

因此,ScopeJoiner 中的所有任务仍然处于当前 ThreadScope 的生命周期内,可以继续使用 scope 已有的能力,包括:

  • deadline 和任务超时;
  • cancellation token;
  • 上下文传播;
  • 重试策略;
  • 任务调度;
  • 执行钩子;
  • 指标采集;
  • scope 关闭时的统一清理。
flowchart TD A[业务代码] --> B[ScopeJoiner] B --> C[JoinStrategy] B --> D[JoinState] D --> E[scope.submit] D --> F[scope.schedule] D --> G[scope.awaitJoinFuture] E --> H[Scheduler] E --> I[ExecutionContextCarrier] E --> J[RetryExecutor] E --> K[ThreadHook] E --> L[ScopeMetrics] E --> M[CancellationToken] F --> N[DelayScheduler] G --> M

ScopeJoiner 只负责组织任务之间的协作关系,任务本身仍由 ThreadScope 统一执行和管理。取消、超时、上下文传播和运行指标不会因为引入高级编排而失效,也不需要重复实现。

写在最后

ScopeJoiner 解决的是更高一层的并发编排问题:它将 firstSuccessquorumhedged 等常见并发模式封装成统一策略,同时继续复用 ThreadScope 已有的取消、超时、上下文传播和观测能力。

无论编排逻辑多复杂,任务最终仍然运行在 ThreadScope 的生命周期边界内。

至此,ThreadForge 的三篇源码解读正式结束。

从第一篇介绍 ThreadScope 如何管理任务边界,到第二篇拆解 Task 的完整执行链路,再到这一篇分析 ScopeJoiner 的并发编排机制,这三层共同组成了 ThreadForge 当前的并发运行时:

  • ThreadScope 负责生命周期和运行边界;
  • Task 负责单个任务的执行过程;
  • ScopeJoiner 负责多个任务之间的协作与结果决策。

最近一段时间工作比较忙,很多精力都投入到了实际业务和团队交付中。

越来越多的业务也开始向 AI 靠拢,我自己也在不断积累和突破 AI 工程化落地方面的实践经验,并在现有业务中做了大量探索。

这些实践不只包括 Function Calling、RAG 和 Agent,也逐渐延伸到了研发流程、运维排障、自动化执行、灰度发布等环节。

过程中既有真正产生价值的方案,也踩过不少看起来厉害、但实际落地效果一般的坑。

后续会结合具体业务场景,逐步整理并分享这些实践,包括技术方案、架构取舍、落地成本,以及哪些场景适合使用 AI,哪些场景并不适合。

最后仍然奉上地址,感谢您的三连:

ThreadForge 项目地址:github.com/wuuJiawei/T...

欢迎提交 issue,也欢迎 star。

相关推荐
无限压榨切图仔1 小时前
从 Claude Code 切到 Codex:我用 Agent、Skills、MCP 做完了一个内容运营工具
前端·后端
董员外2 小时前
RAG 系统进化论(一):纵览 RAG 的发展历程
前端·人工智能·后端
Yao8062 小时前
MyBatis-Plus LambdaQueryWrapper实战:告别手写SQL
后端
FIT2CLOUD飞致云2 小时前
移动端审批功能上线,导入功能持续增强,Cordys CRM发布v1.8.0版本
ai·开源·crm·销售管理·ai crm·cordys crm
街头小霸王6262 小时前
微服务项目common配置文件模板
后端
Conan在掘金2 小时前
ArkTS 进阶之道(17):@Extend 专属属性扩展边界——为啥能收 fontSize 专属属性
后端
霸道流氓气质2 小时前
SpringBoot中事务内同步处理 + 事务后异步调用外部系统的通用模式示例
java·spring boot·后端
开源推荐官3 小时前
本地图切OSS,tigshop开源商城存储驱动切换记录
开源
AskHarries4 小时前
用户埋点怎么设计
后端