MultiThread SkinnedMeshRenderer原理及实现
Posted _Captain
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了MultiThread SkinnedMeshRenderer原理及实现相关的知识,希望对你有一定的参考价值。
最近在思考Unity AUP在OpenGLES里面的实现方式,原理就是OpenGL 多线程多Context状态共享,这个一时还没有比较完善的思路。
不过我又想起另一件事,就是之前写的性能特别差的SkinnedMeshRenerer,性能特别差,主要有2方面的原因:
转自https://huutu.blog.csdn.net/article/details/113787863
1.从3dsMax导出文件时,有很多预计算没有做,全部放在代码中实时运算了。
2.顶点坐标更新是单线程的。
因为电脑上没有3dsmax2019了,所以第一点就先搁置。而这几天又在做多线程的工作,于是计划把第2点解决,即多线程SkinnedMeshRenderer。
1. 先上结果
测试机器:i7 4710HQ 4核8线程
测试方式:同屏播放 101 个女法师待机动画。
首先是单线程,帧率在 21-32 之间浮动,CPU占用 9%。
然后是4线程,帧率在 31-66 之间浮动,CPU占用 35%。
可以看到提升效果显著。
2.移动平台多线程可用性
近5年的手机CPU都是至少4核心了,我搜索到一份2016年低端机型CPU参数表。
转自https://huutu.blog.csdn.net/article/details/113787863
大部分其实已经是8核心了。
3.选择合适的线程管理方式
为了尽快测试,我一开的设计是这样的:
1.在SkinnedMeshRenderer的每一次更新顶点位置时,都创建一个临时线程,然后将顶点位置更新的逻辑在临时线程中进行。
2.创建一个静态变量 threadTaskCount 记录当前子线程数量,创建线程之前 +1,线程逻辑执行完毕后 -1 。
3.在引擎入口 Update() 之后,判断 threadTaskCount 是否为0,如果不为0代表仍然有线程在进行计算,需要等待。当全部线程完成后,进行 Render()。
这一套设计方案逻辑特别简单,很快就验证了多线程SkinnedMeshRenderer 是可行的。
但是问题也是很明显的:
1.创建线程是很重量级的操作,每一次Update 我都创建 101 个线程,这导致时间全部花费在线程创建了。
2.线程数超过CPU核心数,操作系统将花费大量时间在线程调度上。
为了解决上面的问题,引入线程池与任务队列。
3.1 线程池
线程池,简单的说就是个线程数组,当需要用到线程,就从线程数组中拿出来一个干活。
复杂的线程池有更多的功能,不过我这里只需要最简单的。
线程池又关联着2个东西,任务 和 存储任务的容器RingBuffer。
线程是内部自循环的,外部只能把耗时的工作,包装成一个任务扔到线程的任务队列,然后线程内部循环从任务队列中取出来执行。
3.1.1 任务
需求很简单,任务中只需要存储一个无参数无返回值的function即可。
//无参数Callback
typedef std::function<void()> ThreadPoolTaskExecuteFunc;
//子线程任务
class ThreadPoolTask
public:
ThreadPoolTaskExecuteFunc mCallback;
;
3.1.2 RingBuffer
实现一读一写 无锁的RingBuffer。
//无锁RingBuffer 最后一个单元内存不存数据 固定对象大小
//参考https://www.cnblogs.com/hwl1023/p/4946372.html
template <class T>
class RingBuffer
public:
T** mBuffer;//一块内存存储n个T指针
int mSize;//T的个数
int mHead;//头部地址 就是Push的地方
int mTail;//尾部地址 就是Pop的地方
public:
//初始化 申请指定大小 内存块
void Init(int varSize)
mBuffer = new T*[varSize];
mHead = 0;
mTail = 0;
mSize = varSize;
bool IsEmpty()
if (mTail == mHead)
return true;
return false;
bool IsFull()
if ((mTail + 1) % mSize == mHead)
return true;
return false;
//添加一个对象
bool Push(T* varObjectPtr)
if (IsFull())
return false;
//拷贝对象数据到以 mTail 为起始地址的内存块
mBuffer[mTail] = varObjectPtr;
//mTail移动
mTail = (mTail + 1) % mSize;
return true;
//抛出一个对象
T* Pop()
if (IsEmpty())
return nullptr;
T* tmpObjectPtr = mBuffer[mHead];
mHead = (mHead + 1) % mSize;
return tmpObjectPtr;
;
3.1.3 封装Thread
标准库的Thread 还是需要再封装一下的,至少提供一个Pause接口吧。
//搬运自 https://blog.csdn.net/hai7song/article/details/104943975
class EngineThread
private:
bool mIsStop;
bool mIsPause;
std::condition_variable mConditionVariable;
std::mutex mMutex;
int mID;
std::function<void(int)> mThreadExecuteFunc;
public:
EngineThread() :mIsStop(false), mIsPause(true), mID(0);
void SetID(int varID) mID = varID;
int GetID()return mID;
void Create(std::function<void(int)> varThreadExecuteFunc)
mThreadExecuteFunc = varThreadExecuteFunc;
std::thread t(
[&]
while (!mIsStop)
mThreadExecuteFunc(mID);
//std::this_thread::sleep_for(std::chrono::seconds(1));
std::unique_lock<std::mutex> lock(mMutex);
mConditionVariable.wait(lock, [this] return !mIsPause; );
);
t.detach();
void Pause()
std::unique_lock<decltype(mMutex)> l(mMutex);
mIsPause = true;
mConditionVariable.notify_one();
void Resume()
std::unique_lock<decltype(mMutex)> l(mMutex);
mIsPause = false;
mConditionVariable.notify_one();
void Stop()
mIsStop = true;
;
转自https://huutu.blog.csdn.net/article/details/113787863
3.1.4 线程池设计
主要提供以下接口:
Init 创建线程并指定线程中执行的函数,初始化RingBuffer指定尺寸
AddTask 添加任务
IsEmpty 判断是否任务队列为空/任务全部完成
class ThreadPool
private:
static int mThreadCount;//线程数量
static std::vector<EngineThread*> mThreadVec;//线程列表
static std::vector<RingBuffer<ThreadPoolTask>*> mThreadTaskRingBufferVec;//线程对应的任务RingBuffer列表
static int mTaskIndex;
static int mDispatchThreadIndex;//下一个接受任务的线程Index
public:
//初始化指定数量线程
static void Init(int varThreadCount)
mThreadCount = varThreadCount;
//初始化线程任务队列
for (size_t i = 0; i < varThreadCount; i++)
RingBuffer<ThreadPoolTask>* tmpRingBufferPtr = new RingBuffer<ThreadPoolTask>();
tmpRingBufferPtr->Init(200);
mThreadTaskRingBufferVec.push_back(tmpRingBufferPtr);
for (size_t i = 0; i < varThreadCount; i++)
EngineThread* t = new EngineThread();//创建一个子线程
t->SetID(i);
t->Create([=](int varThreadID)
ThreadRun(varThreadID);
);
mThreadVec.push_back(t);
//添加一个任务
static void AddTask(ThreadPoolTask* varThreadPoolTask)
//当前任务Index 归属于 哪个线程Index
mDispatchThreadIndex = mTaskIndex%mThreadCount;
//将任务归属到对应线程
RingBuffer<ThreadPoolTask>* tmpRingBuffer = mThreadTaskRingBufferVec[mDispatchThreadIndex];
while (tmpRingBuffer->Push(varThreadPoolTask) == false)
//如果RingBuffer满了,就等待
std::this_thread::sleep_for(std::chrono::nanoseconds(1));
//激活对应线程
mThreadVec[mDispatchThreadIndex]->Resume();
mTaskIndex++;
//是否没有任务
static bool IsEmpty()
for (size_t i = 0; i < mThreadCount; i++)
RingBuffer<ThreadPoolTask>* tmpRingBuffer = mThreadTaskRingBufferVec[mDispatchThreadIndex];
if (tmpRingBuffer->IsEmpty() == false)
return false;
return true;
private:
//在子线程中执行的函数
static void ThreadRun(int varThreadID)
//获取当前子线程的任务RingBuffer
RingBuffer<ThreadPoolTask>* tmpRingBuffer = mThreadTaskRingBufferVec[varThreadID];
//没有任务就等待然后return
if(tmpRingBuffer->IsEmpty())
std::this_thread::sleep_for(std::chrono::nanoseconds(1));
return;
//抛出一个任务
ThreadPoolTask* tmpThreadPoolTask = tmpRingBuffer->Pop();
//执行
tmpThreadPoolTask->mCallback();
;
3.1.5 线程池使用
初始化
//初始化线程池
ThreadPool::Init(4);
添加任务
ThreadPoolTask* tmpThreadPoolTask = new ThreadPoolTask();
tmpThreadPoolTask->mCallback = [=]()
UpdateMesh();
;
ThreadPool::AddTask(tmpThreadPoolTask);
主线程自旋,判断所有任务是否完成。
while (true)
if (ThreadPool::IsEmpty())
break;
//Sleep(1);
转自https://huutu.blog.csdn.net/article/details/113787863
4. 相关代码
线程池
https://git.code.tencent.com/ThisisGame/DreamEngine/blob/6247d333dab05f58c92383c67756709df841703a/Engine/Src/Tools/ThreadPool.h
SkinnedMeshRenderer
https://git.code.tencent.com/ThisisGame/DreamEngine/blob/6247d333dab05f58c92383c67756709df841703a/Engine/Src/3D/SkinMeshRenderer.cpp
主逻辑入口
https://git.code.tencent.com/ThisisGame/DreamEngine/blob/6247d333dab05f58c92383c67756709df841703a/Engine/Src/Platform/Windows/main.cpp
以上是关于MultiThread SkinnedMeshRenderer原理及实现的主要内容,如果未能解决你的问题,请参考以下文章
MultiThread(VS2013 MFC多线程-含源码-含个人逐步实现文档)
Multithread 什么时候处理多线程,有几种方式,优缺点?