# AsyncExecutor 协作式取消设计 ## 背景 `lsp.scheduler.async_executor` 当前通过 Taskflow 执行后台任务,但任务句柄只 弱引用执行状态,快速任务完成后句柄立即失效;句柄取消按任务 ID 回查,可能 误取消同 ID 的替代任务;排队任务即使已取消仍会执行。任务注册表同时承担 “当前 ID”与“全部活跃任务”两种职责,导致等待、统计和重复 ID 语义不稳定。 本设计不保留现有 API 兼容层,直接建立明确的任务生命周期、结果和协作式 取消契约。 ## 目标 - 任务句柄在任务完成后仍可等待并读取最终结果。 - 句柄取消只影响该句柄对应的任务实例。 - 按 ID 取消只影响该 ID 当前注册的任务实例。 - 已取消的排队任务不执行任务体。 - 运行中的长任务通过 `std::stop_token` 协作退出。 - 重复 ID 的全部活跃任务被准确等待和计数。 - 任务成功、取消和失败使用单一结果模型表达。 - 隐藏执行状态、同步原语和句柄构造细节。 - API namespace 与 Module 名保持一致。 ## 非目标 - 不强制终止正在运行的线程。 - 不为旧的 `TaskClosure`、callback 或 `WaitForTask` 提供兼容重载。 - 不在本任务中启用 LSP `$/cancelRequest` 的请求级异步分发;本设计只保证 scheduler 及其后台索引任务具备正确取消能力。 - 不修改 Taskflow 本身。 ## 公共 API Module 名保持: ```cpp export module lsp.scheduler.async_executor; ``` 公共 API 迁移到: ```cpp namespace lsp::scheduler::async_executor ``` 任务结果统一为: ```cpp enum class TaskStatus { kCompleted, kCancelled, kFailed, }; struct TaskResult { TaskStatus status; std::optional value; std::exception_ptr error; }; ``` 任务 closure 只产生业务值,最终状态由 executor 根据停止请求和异常统一 生成,避免任务体自行构造相互矛盾的状态: ```cpp using TaskClosure = std::function(std::stop_token)>; using TaskCallback = std::function; ``` `TaskHandle` 对外提供: ```cpp bool Valid() const; bool Cancel() const; std::optional Wait() const; std::optional TryGetResult() const; ``` - 默认构造句柄无效,`Wait` 与 `TryGetResult` 返回 `std::nullopt`。 - 有效句柄完成后仍保持有效。 - `Cancel` 仅在首次成功请求取消且任务尚未完成时返回 `true`。 - `Wait` 等待任务体和 callback 都结束,并返回最终任务结果。 - `TryGetResult` 不阻塞;尚未完成时返回 `std::nullopt`。 - 构造函数为私有,仅 `AsyncExecutor` 可以创建有效句柄。 删除 `AsyncExecutor::WaitForTask`。调用方必须保存 `TaskHandle` 来等待特定 任务;按字符串 ID 只用于取消当前实例,不承担历史结果查询。 ## 内部状态与所有权 `TaskHandle` 通过 `std::shared_ptr` 强持有私有嵌套状态。状态至少包含: - `std::stop_source`:生成 stop token 并接受取消请求。 - `TaskPhase`:`kPending`、`kRunning`、`kCompleted`。 - `std::optional`:最终结果。 - mutex、condition variable 和 callback 完成标记。 - task ID 与开始时间。 状态定义不导出,外部不能构造或修改同步字段。 `AsyncExecutor` 使用两份索引: - `current_tasks_`:`task_id -> state`,只表示该 ID 当前实例,供 `Cancel(id)` 使用。 - `active_tasks_`:以任务实例唯一标识保存全部未完成状态,供 `WaitAll`、`GetRunningTaskCount` 与统计使用。 提交同 ID 新任务时,请求旧实例停止,然后替换 `current_tasks_`;旧实例仍 留在 `active_tasks_`,直到任务体和 callback 都完成。 ## 取消与执行流程 ### 排队取消 worker 获取任务状态后,在持锁状态下检查 stop token: - 已请求停止:不调用 closure,直接产生 `kCancelled`。 - 未请求停止:状态从 `kPending` 转为 `kRunning`,随后调用 closure。 因此取消和开始执行之间只有一个明确的同步竞态点;取消先获得状态锁时, 任务体保证不会执行。 ### 运行中取消 任务开始后,`Cancel` 调用 `stop_source.request_stop()`。executor 不强制停止 线程;closure 接收 `std::stop_token`,在文件枚举、索引和符号加载循环的 安全边界调用 `stop_requested()` 并尽快返回。 如果运行中收到停止请求,即使 closure 已产生普通值,executor 仍将最终 状态归类为 `kCancelled`,避免向 callback 报告成功。 ### 完成与 callback executor 捕获任务异常并生成 `kFailed`,保留原始 `exception_ptr`。任务结果 先写入状态,再调用 callback;callback 异常被捕获并记录,但不改写已经确定 的任务结果。只有 callback 返回或异常被处理后,状态才标记为完全完成、从 两个注册表注销并唤醒等待者。 callback 不得从自身调用 `Wait` 或 `WaitAll`;该约束写入接口注释和测试命名, 避免自等待死锁。 ## 并发数与生命周期 传入并发数 0 或 `std::thread::hardware_concurrency()` 返回 0 时,实际 worker 数收敛到 1,再构造 `tf::Executor`。 `AsyncExecutor` 析构时调用 `WaitAll`。`WaitAll` 等待所有活跃状态完成,并再 调用 Taskflow `wait_for_all()`,确保 callback 中提交的后续任务也已退出。 析构期间不允许其他线程继续调用 `Submit`;这是对象生命周期的基本前置条件。 ## 调用方迁移 所有 `scheduler::AsyncExecutor`、`scheduler::TaskHandle` 等引用迁移到 `scheduler::async_executor::*`。所有 `Submit` closure 增加 `std::stop_token` 参数,callback 改为接收 `const TaskResult&`。 工作区加载、系统库加载、文件索引和 workspace folder 变更任务将 stop token 传入长循环;循环在每个文件或目录边界检查停止请求。只执行一次且不可拆分的 第三方调用在调用前后检查 token,不尝试中断其内部线程。 ## 清理 - 删除公开的 `detail::ExecutionState` 与 `detail::ActiveEntry`。 - 将 `ExecutorMetrics` 作为明确公共结果类型保留在 `lsp::scheduler::async_executor`。 - 删除未使用的 `ActiveEntry::callback`、`ActiveEntry::start_time` 和 `kStatusLogInterval`。 - 测试 Module 的入口同步放入自身命名空间,不再导出全局 `Run`。 ## 测试设计 测试遵循 TDD,先在旧实现上观察失败,再实现新语义: - 无延迟任务完成后,handle 仍有效且可读取结果。 - `Wait` 返回任务体与 callback 完成后的最终结果。 - 同 ID 替换后,旧 handle 取消不影响新任务。 - 排队任务被取消后,任务体从未执行。 - 运行中任务观察 stop token 并协作退出。 - 同 ID 两个活跃实例被计数为 2,`WaitAll` 等待两者。 - 并发数 0 可以正常执行任务。 - task 异常生成 `kFailed` 并保留异常。 - callback 异常不会导致等待死锁,也不会改写任务结果。 - 成功、取消、失败统计与最终状态一致。 迁移完成后运行 scheduler 测试、provider 测试和生产服务器构建;LSP 传输 冒烟测试确认后台初始化和 workspace 操作没有回归。 ## 完成条件 - 快速完成任务的 handle 不失效。 - 任一 handle 只能取消自己的状态。 - 已取消的排队 closure 不执行。 - 长任务可以通过 stop token 提前退出。 - `GetRunningTaskCount` 和 `WaitAll` 覆盖重复 ID 的所有实例。 - 不存在 `WaitForTask`、公开执行状态或未使用调度字段。 - 并发数始终至少为 1。 - namespace、调用方和测试全部迁移完成。 - 约定范围内构建与测试通过。