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

163 lines
6.6 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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)