不能靠“开三个 std::thread 就完事”,必须按职责隔离、数据同步机制、内存访问约束三者协同,否则大概率出现帧抖动、资源损坏或死锁。

直接说结论:不能靠“开三个 std::thread 就完事”,必须按职责隔离 + 数据同步机制 + 内存访问约束三者协同,否则大概率出现帧抖动、资源损坏或死锁。
主线程、渲染线程、物理线程到底该各干啥
职责混在一起是并发问题的根源。现代引擎(如 Unreal、Frostbite)实际采用的是“单写多读 + 帧延迟交换”模型:
- 主线程只负责游戏逻辑更新(输入响应、AI状态机、脚本执行)、场景图变更(添加/删除实体),不碰任何 GPU 资源或物理世界句柄
- 物理线程独占
PhysX或Bullet的 world 实例,每帧调用stepSimulation(),输出碰撞对和刚体变换结果到**只写缓冲区** - 渲染线程不生成命令列表(那是主线程或专用任务线程干的),只做两件事:
ExecuteCommandLists()提交、Present()交换帧缓冲
关键点:物理线程和渲染线程都不直接修改主线程的数据结构,而是通过结构化输出(如 PhysicsResultBuffer、RenderCommandBuffer)传递结果。
怎么避免三个线程抢同一块内存
共享指针、全局变量、裸指针跨线程传地址——这三种写法在多线程渲染里等于埋雷。真正安全的做法是:
立即学习“C++免费学习笔记(深入)”;
- 用双缓冲 + 原子索引切换:例如
std::atomic<int> g_frameIndex{0}</int>,主线程写buffer[g_frameIndex.load() % 2],物理线程读上一帧的 buffer,渲染线程读再上一帧的 buffer - 物理结果不存指针,存 ID 和变换矩阵:比如
struct PhysicsUpdate { uint32_t entityId; glm::mat4 transform; },避免跨线程引用生命周期不确定的对象 - 纹理/顶点缓冲等 GPU 资源必须在渲染线程创建并持有,主线程只能通过资源 ID(
uint64_t)间接引用,加载完成后再用原子标志通知
常见错误现象:Access violation reading location 0x0000000000000000,往往是因为主线程删了实体,但物理线程还在用其指针遍历碰撞体。
std::mutex 和 std::atomic 到底用哪个
不是“能用原子就不用锁”,而是看操作粒度和语义:
- 开关类标志(如
isReady、hasNewData):必须用std::atomic<bool></bool>,配合memory_order_acquire/release,否则编译器/CPU 可能重排指令导致读到脏值 - 写入一个结构体数组(如 500 个实体的变换矩阵):用
std::mutex保护整个写入过程,别试图原子化每个字段——glm::mat4是 16 字节,x86-64 不保证 16 字节原子写 - 队列 push/pop:优先选无锁队列(如
boost::lockfree::queue或自己实现 Michael-Scott),std::queue+std::mutex在高频命令提交时会成为瓶颈(实测 lock/unlock 占单帧 0.3ms+)
性能影响明显:在 12 核 CPU 上,用互斥锁保护每帧 2000 次 Draw Call 注册,帧时间比无锁队列高 1.7ms;而用 std::atomic_flag 控制双缓冲切换,开销几乎为零。
为什么你的三线程跑不满 CPU,还更卡了
线程数 ≠ 性能提升。真实瓶颈常藏在三个地方:
- 主线程仍承担剔除(frustum culling)和排序(transparent draw order)——这些完全可以拆成任务扔进线程池,而不是绑死在主线程
- 物理线程和渲染线程都频繁访问同一块 L3 缓存行(比如共用一个
EntityComponentArray<transform></transform>),引发伪共享(false sharing),实测可使吞吐下降 40% - GPU 提交太慢,CPU 线程却拼命塞命令:没加
ID3D12Fence或VkSemaphore控制提交节奏,导致 GPU 队列积压,Present()被阻塞,所有线程都在等 vsync
最容易被忽略的一点:三个线程默认调度策略都是 SCHED_OTHER,Linux 下可能被挤到同一物理核;Windows 下没显式设置 SetThreadAffinityMask(),也可能导致核心争抢。真要榨干多核,得手动绑核 + 调整优先级。



















