Files
XCEngine/docs/api/_guides/Threading/Synchronization-And-TaskSystem-Limits.md

163 lines
6.6 KiB
Markdown
Raw Permalink Normal View History

2026-03-26 20:59:59 +08:00
# 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)