上一篇写了两个OpenGL逻辑与渲染分离的多线程错误架构示例,这篇来写写正确的。这篇并不像上篇是纯理论,而是会附一些代码,但代码里可能有不够优雅或有内存风险的地方,希望大家多多批评~
上一篇在这里:
Frandium:OpenGL渲染线程与逻辑线程分离的错误架构示例zhuanlan.zhihu.com阅读此文前你应该知道
- OpenGL(基于glfw和glad库)
- pthread或其他你熟悉的多线程库
- C++
- OpenGL多线程渲染的基本事实(见上一篇)
阅读此文后你会了解
- 使用pthread创建和进行线程同步
- 基于双队列swap的渲染与逻辑分离架构设计
- OpenGL多线程的坑和注意点
架构纵览
要实现多线程渲染,就需要使用一个缓冲队列存储渲染指令。逻辑线程向其中添加指令,渲染线程消费。由于传统生产者-消费者模式要求在入队出队时都要加锁,这会导致逻辑线程和渲染线程不停地被挂起唤醒(或忙等),效率很低,因此我们希望逻辑线程生产指令和渲染线程消费指令不要相互竞争。这启发我们使用双队列方案:使用两个队列,一个供逻辑线程添加指令,一个供渲染线程取出指令。在每帧结束后(或开始前),交换两个队列。也就是说,渲染永远比逻辑慢1帧。整体的系统结构图如下:
![c04f1df5fb4595ba5e2cd16ba645cca3.png](https://i-blog.csdnimg.cn/blog_migrate/af33a0ce981c5a6a4ad297f5e006bf5c.jpeg)
注意到Mono::Init()回调是在渲染线程初始化后才执行的。因为init中会存在一些绑定VBO、编译Shader之类的操作,因此一定要放在渲染context生成之后。
接下来依次说明同步的实现、渲染队列的实现细节和内存有关的问题。
同步
同步是使用pthread中barrier实现的。由于每一帧各个线程执行顺序不确定,而我们希望barrier在被各个线程wait barrier之前就已经被初始化好。因此我们使用两个barrier,一开始都初始化。接下来每一帧让主线程把下一帧要用到的barrier初始化,然后大家一起wait这一帧的barrier。具体操作的方法就是维护一个帧计数器,然后对2取余,经典的ping-pong操作。
主线程:
void Engine::Run(){
#ifndef MULTITHREAD
// 单线程的逻辑
#endif
#ifdef MULTITHREAD
int frame_count = 0;
pthread_t logical, render;
// 2 barriers to synchronize main thread, logical thread and render thread;
pthread_barrier_t barriers[2];
pthread_barrier_init(barriers, NULL, 3);
pthread_barrier_init(barriers + 1, NULL, 3);
int ret = pthread_create(&logical, NULL, LogicalThread, &barriers);
if (ret < 0) {
std::cout << "Failed to create logical thread." << std::endl;
return;
}
ret = pthread_create(&render, NULL, RenderThread, &barriers);
if (ret < 0) {
std::cout << "Failed to create render thread." << std::endl;
return;
}
pthread_barrier_wait(barriers + frame_count % 2);
#endif
std::cout << "m