6.6 KiB
6.6 KiB
Synchronization And Task System Limits
先建立正确的心智模型
XCEngine::Threading 当前不是一套已经完整收口的商业级并发框架,而是三层能力并存:
- 同步原语层: Mutex、SpinLock、ReadWriteLock
- 线程包装层: Thread
- 任务抽象层: ITask、TaskGroup、TaskSystem
这三层同时存在是合理的。商业引擎里也几乎从来不是“有了 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 里写可靠并发代码,更稳妥的做法是:
- 基础同步优先用 Mutex 或 ReadWriteLock。
- 极短临界区再考虑 SpinLock。
- 需要明确线程生命周期的后台服务,优先直接用 Thread。
- 当前把 TaskSystem 和 TaskGroup 视为实验性骨架,而不是生产级调度器。
如果要把它演进到商业级,需要补什么
优先级应该是:
- 修正任务所有权和引用计数协议。
- 修正队列同步,消除数据竞争。
- 实现真实的任务完成等待。
- 让
TaskGroup的依赖、进度和完成回调真正接入调度器。 - 明确
Shutdown()/ reinitialize / cancel 的状态机。 - 最后再补 profiling、work stealing、局部队列和更智能的
ParallelFor。
顺序不能反。商业级任务系统最先要站稳的是正确性,其次才是性能和功能丰富度。