# Synchronization And Task System Limits ## 先建立正确的心智模型 `XCEngine::Threading` 当前不是一套已经完整收口的商业级并发框架,而是三层能力并存: - 同步原语层: [Mutex](../../XCEngine/Threading/Mutex/Mutex.md)、[SpinLock](../../XCEngine/Threading/SpinLock/SpinLock.md)、[ReadWriteLock](../../XCEngine/Threading/ReadWriteLock/ReadWriteLock.md) - 线程包装层: [Thread](../../XCEngine/Threading/Thread/Thread.md) - 任务抽象层: [ITask](../../XCEngine/Threading/Task/Task.md)、[TaskGroup](../../XCEngine/Threading/TaskGroup/TaskGroup.md)、[TaskSystem](../../XCEngine/Threading/TaskSystem/TaskSystem.md) 这三层同时存在是合理的。商业引擎里也几乎从来不是“有了 job system 就不要锁了”,原因很简单: - 平台窗口消息、日志、资源表、对象注册表,仍然需要细粒度同步。 - 有些后台工作需要明确线程生命周期,而不是匿名 job。 - 任务系统更多解决吞吐量和调度问题,不会替代所有底层同步。 ## 为什么要同时保留锁、线程和任务系统 可以把它类比成很多商业引擎的常见分层: - 最底层保留 `mutex`、`rw lock`、原子变量等基础设施。 - 中间层保留明确的 worker thread 或 service thread,例如文件监控线程、日志线程、调试采样线程。 - 上层再把大量短小、彼此独立的工作投递到 job system。 从设计意图上看,XCEngine 当前也在往这个方向走: - `Mutex` 负责通用互斥,适合临界区较大或等待时间不可预测的场景。 - `SpinLock` 适合极短临界区,代价是忙等。 - `ReadWriteLock` 试图提升读多写少场景的吞吐量。 - `Thread` 把 `std::thread` 的基本生命周期包装成统一接口。 - `TaskSystem` 则意图成为更高层的统一调度器。 ## 这些同步原语各自适合什么 ### `Mutex` 它本质上是对 `std::mutex` 的轻量包装。 适合: - 保护 map、vector、日志缓冲区等普通共享状态。 - 需要和 `std::lock_guard`、`std::unique_lock` 等标准库工具协作的场景。 优点: - 语义清晰。 - 平台和标准库行为成熟。 代价: - 阻塞时会进入内核或调度器等待路径,开销比自旋更高。 ### `SpinLock` 它当前用 `atomic_flag` 做空循环忙等,没有退避、没有 `yield()`、没有公平性控制。 适合: - 临界区极短。 - 竞争概率低。 - 线程数和 CPU 核数比较接近。 不适合: - 主线程容易被卡住的路径。 - 高竞争场景。 - 持锁期间有任何可能阻塞的代码。 ### `ReadWriteLock` 当前实现采用写者优先策略。设计动机很好理解: - 对引擎里的配置表、资源索引、场景查询类结构,读远多于写时,读写锁通常比普通互斥更有吸引力。 但当前实现要注意: - 当有写者排队时,新读者会被挡住。 - 这会降低写饥饿风险,但提高读饥饿风险。 这是典型的吞吐量与公平性权衡。 ## `Thread` 的定位是什么 `Thread` 当前不是线程框架,只是对 `std::thread` 的轻量包装。 它的价值主要在于: - 统一接口风格。 - 在对象内保存一个引擎层名称和一个缓存 ID。 - 析构时自动 `join()`,减少“忘记回收线程句柄”的错误。 但这也意味着: - 线程名当前不会传播到操作系统。 - `GetId()` 与 `GetCurrentId()` 的 ID 口径并不一致。 - 如果错误地对仍然 `joinable` 的对象再次 `Start()`,行为风险和原生 `std::thread` 一样高。 ## `TaskSystem` 为什么现在还不能算商业级 job system 如果拿成熟引擎的任务系统做参照,至少会期待这些基本性质: - 任务对象生命周期绝对正确。 - 队列并发访问无数据竞争。 - 支持明确的等待、依赖和 completion fence。 - 关闭语义清晰。 - 线程数、队列、分块策略和 profiling 都能受配置控制。 而当前源码距离这些目标还有明显差距: - `Submit()` 会把裸指针压入队列,但提交函数返回时对象可能已经被销毁。 - 任务队列的入队和出队没有统一使用同一把锁。 - `Wait()` 是空实现。 - `TaskGroup` 的依赖、完成计数、完成回调都没有真正接上。 - `ParallelFor()` 不等待结果,只是批量投递。 - `Shutdown()` 只停线程,不真正清理任务生命周期。 这不是文档措辞问题,而是正确性问题。对于并发系统来说,正确性比 API 漂不漂亮重要得多。 ## 那为什么还要保留这些接口 因为这套接口形状本身是有价值的,它表达了引擎未来很可能需要的能力: - 批处理任务提交。 - 主线程回投。 - 任务组与依赖图。 - 并行 for。 - 可扩展的调度配置。 很多商业引擎也是先稳定接口轮廓,再逐步把执行器、profiling、依赖图和取消语义补齐。当前文档保留这些页,不是为了假装能力已经可用,而是为了把“设计目标”和“当前现实”同时讲清楚。 ## 当前阶段的推荐实践 如果你现在要在 XCEngine 里写可靠并发代码,更稳妥的做法是: 1. 基础同步优先用 [Mutex](../../XCEngine/Threading/Mutex/Mutex.md) 或 [ReadWriteLock](../../XCEngine/Threading/ReadWriteLock/ReadWriteLock.md)。 2. 极短临界区再考虑 [SpinLock](../../XCEngine/Threading/SpinLock/SpinLock.md)。 3. 需要明确线程生命周期的后台服务,优先直接用 [Thread](../../XCEngine/Threading/Thread/Thread.md)。 4. 当前把 [TaskSystem](../../XCEngine/Threading/TaskSystem/TaskSystem.md) 和 [TaskGroup](../../XCEngine/Threading/TaskGroup/TaskGroup.md) 视为实验性骨架,而不是生产级调度器。 ## 如果要把它演进到商业级,需要补什么 优先级应该是: 1. 修正任务所有权和引用计数协议。 2. 修正队列同步,消除数据竞争。 3. 实现真实的任务完成等待。 4. 让 `TaskGroup` 的依赖、进度和完成回调真正接入调度器。 5. 明确 `Shutdown()` / reinitialize / cancel 的状态机。 6. 最后再补 profiling、work stealing、局部队列和更智能的 `ParallelFor`。 顺序不能反。商业级任务系统最先要站稳的是正确性,其次才是性能和功能丰富度。 ## 相关 API - [Threading](../../XCEngine/Threading/Threading.md) - [Mutex](../../XCEngine/Threading/Mutex/Mutex.md) - [SpinLock](../../XCEngine/Threading/SpinLock/SpinLock.md) - [ReadWriteLock](../../XCEngine/Threading/ReadWriteLock/ReadWriteLock.md) - [Thread](../../XCEngine/Threading/Thread/Thread.md) - [Task](../../XCEngine/Threading/Task/Task.md) - [TaskGroup](../../XCEngine/Threading/TaskGroup/TaskGroup.md) - [TaskSystem](../../XCEngine/Threading/TaskSystem/TaskSystem.md)