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

6.6 KiB
Raw Blame History

Synchronization And Task System Limits

先建立正确的心智模型

XCEngine::Threading 当前不是一套已经完整收口的商业级并发框架,而是三层能力并存:

这三层同时存在是合理的。商业引擎里也几乎从来不是“有了 job system 就不要锁了”,原因很简单:

  • 平台窗口消息、日志、资源表、对象注册表,仍然需要细粒度同步。
  • 有些后台工作需要明确线程生命周期,而不是匿名 job。
  • 任务系统更多解决吞吐量和调度问题,不会替代所有底层同步。

为什么要同时保留锁、线程和任务系统

可以把它类比成很多商业引擎的常见分层:

  • 最底层保留 mutexrw lock、原子变量等基础设施。
  • 中间层保留明确的 worker thread 或 service thread例如文件监控线程、日志线程、调试采样线程。
  • 上层再把大量短小、彼此独立的工作投递到 job system。

从设计意图上看XCEngine 当前也在往这个方向走:

  • Mutex 负责通用互斥,适合临界区较大或等待时间不可预测的场景。
  • SpinLock 适合极短临界区,代价是忙等。
  • ReadWriteLock 试图提升读多写少场景的吞吐量。
  • Threadstd::thread 的基本生命周期包装成统一接口。
  • TaskSystem 则意图成为更高层的统一调度器。

这些同步原语各自适合什么

Mutex

它本质上是对 std::mutex 的轻量包装。

适合:

  • 保护 map、vector、日志缓冲区等普通共享状态。
  • 需要和 std::lock_guardstd::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. 基础同步优先用 MutexReadWriteLock
  2. 极短临界区再考虑 SpinLock
  3. 需要明确线程生命周期的后台服务,优先直接用 Thread
  4. 当前把 TaskSystemTaskGroup 视为实验性骨架,而不是生产级调度器。

如果要把它演进到商业级,需要补什么

优先级应该是:

  1. 修正任务所有权和引用计数协议。
  2. 修正队列同步,消除数据竞争。
  3. 实现真实的任务完成等待。
  4. TaskGroup 的依赖、进度和完成回调真正接入调度器。
  5. 明确 Shutdown() / reinitialize / cancel 的状态机。
  6. 最后再补 profiling、work stealing、局部队列和更智能的 ParallelFor

顺序不能反。商业级任务系统最先要站稳的是正确性,其次才是性能和功能丰富度。

相关 API