为什么要将游戏的渲染线程和逻辑线程分离?
游戏中渲染是一个非常耗时的操作,特别是相对复杂的游戏,渲染通常会占据一帧中的大部分时间。而高品质的游戏都会要求FPS在60,所以一帧的时间仅仅16毫秒。
如果要在16毫秒内完成逻辑和渲染双重的任务,对于大型游戏来说,通常是艰难的,即使在极度优化的情况下,也可能只能满足性能较好的设备,在性能较差的设备上,几乎不可能在16毫秒内完成所有任务。
所以如果将渲染线程和逻辑线程分离,那么理论上,他们各自都有16毫秒的时间来完成各自的任务,因为他们是并行进行的,这样就可以有更多时间来进行渲染和逻辑的执行了。
如果在单线程中,我们通常会先进行逻辑的任务,然后在进行渲染,因为渲染的数据通常依赖逻辑计算的结果。
现在分离成两个线程,我们依然需要逻辑线程先完成任务,然后渲染线程再开始渲染的任务,因此这里就涉及到了线程的同步。
利用C++11中的条件变量,实现一个游戏中逻辑线程和渲染线程同步的问题。
条件变量是多线程的同步的一种常用方法:
C++中的std::condition_variable可以用于声明一个条件变量,他的核心方法是wait:
void wait( std::unique_lock<std::mutex>& lock );
void wait( std::unique_lock<std::mutex>& lock, Predicate pred );
参数1是一个unique_lock类型的锁,用于锁住一个互斥量,参数2通常是一个返回bool值得函数。
如果只有第一个参数,那么wait方法会在被唤醒的时候,去尝试锁住互斥量,如果拿不到互斥量,就会阻塞该线程,如果成功拿到互斥量,就会继续执行该线程,线程执行结束后应该释放该互斥量,也就是解锁。
如果还有第二个参数,那么wait方法会在被唤醒的时候,去检查条件是否满足,也就是第二个参数的返回值是否为true,如果条件满足,上锁,继续执行线程,如果条件不满足,阻塞线程,并解锁,即让出互斥量。
示例如下:
为了方便检测,FPS是每秒一帧,渲染任务和逻辑任务也通过睡眠函数来模拟执行时间,都是1秒,通过打印可以看出来,逻辑和渲染线程的任务并行在执行,渲染线程统计出消耗时间是1秒钟左右。
整个流程大致是:
1. 主线程即渲染线程启动,进行一次空渲染,因为这时候逻辑任务还没有开始执行,即没有任何需要渲染的东西;
2. 第一次空渲染不会被阻塞,渲染线程在执行真正的渲染任务之前,唤醒逻辑线程;
3. 渲染任务开始执行,逻辑任务也并行开始执行;
4. 渲染任务结束,检查下一次渲染时间是否到来。如果没有到来,短时间睡眠等待渲染时间到来;
5. 渲染时间到来,在开始真正的渲染任务前,阻塞检查条件变量是否满足条件,即逻辑任务是否完成。如果逻辑任务还没完成,阻塞等待被逻辑线程唤醒。如果逻辑任务完成,继续回到2的流程;
如果将逻辑任务的执行时间改为2秒,那么渲染线程统计出来的时间将是2秒。因为每一次逻辑任务都是在为下一帧渲染做准备,渲染线程会等待逻辑任务的完成才会进行下一次渲染,所以渲染线程统计出来的时间是逻辑线程和渲染线程所用时间中的最大值,而如果在单线程中,所用时间将是逻辑和渲染执行时间之和。
// ThreadTest3.cpp : 定义控制台应用程序的入口点。 // #include "stdafx.h" #include <mutex> #include <condition_variable> #include <chrono> #include <thread> using namespace std; mutex logic_mt; // 互斥量,在执行逻辑任务的时候上锁,在解锁之前,下一次的渲染任务必须阻塞并等待被唤醒 condition_variable logic_cv; // 逻辑线程的条件变量,满足条件后被渲染线程唤醒 condition_variable render_cv; // 渲染线程的条件变量,满足条件后被逻辑线程唤醒 bool to_do_logic = false; // true表示满足执行逻辑任务的条件,false表示不满足 bool to_do_render = false; // true表示满足执行渲染任务的条件,false表示不满足 // 逻辑线程 void logic_thread_proc() { while (true) { // 阻塞,等待渲染线程的定时唤醒 unique_lock<mutex> lock(logic_mt); logic_cv.wait(lock, [](){ return to_do_logic; }); to_do_logic = false; // 该条件只能被渲染线程重置 printf("_____________ logic begin "); // 逻辑任务。。。 this_thread::sleep_for(chrono::microseconds(1)); printf("_____________ logic "); // 逻辑任务完成,唤醒渲染线程 to_do_render = true; lock.unlock(); render_cv.notify_one(); printf("_____________ logic end "); } } // 主线程 int _tmain(int argc, _TCHAR* argv[]) { thread logic_thread(logic_thread_proc); // 渲染线程先启动,驱动逻辑线程 to_do_render = true; bool last_to_do_render = true; chrono::system_clock::time_point _clock = chrono::high_resolution_clock::now(); while (true) { long long microsec = chrono::duration_cast<chrono::microseconds>(chrono::high_resolution_clock::now() - _clock).count(); // FPS是每秒一帧 if (microsec >= 1) { _clock = chrono::high_resolution_clock::now(); // 阻塞,等待逻辑线程的任务完成 unique_lock<mutex> lock(logic_mt); render_cv.wait(lock, [](){ return to_do_render; }); to_do_render = false; // 该条件只能被逻辑线程重置 printf(" _____________ render begin "); // 唤醒逻辑线程,并行进行下一帧的逻辑任务 to_do_logic = true; lock.unlock(); logic_cv.notify_one(); // 渲染任务。。。 this_thread::sleep_for(chrono::microseconds(1)); printf("_____________ render %lld ", microsec); // 如果在这里唤醒,渲染任务和逻辑任务将不是并行的,而是串行的 // to_do_logic = true; // logic_cv.notify_one(); printf("_____________ render end "); } else { this_thread::sleep_for(chrono::milliseconds(0)); // 暂时休眠,让出CPU } } return 0; }