最近在思考Unity AUP在OpenGLES里面的实现方式,原理就是OpenGL 多线程多Context状态共享,这个一时还没有比较完善的思路。
不过我又想起另一件事,就是之前写的性能特别差的SkinnedMeshRenerer,性能特别差,主要有2方面的原因:
转自
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参数表。
转自
大部分其实已经是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 最后一个单元内存不存数据 固定对象大小
//参考
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接口吧。
//搬运自
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;
}
};
转自
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);
}
转自
4. 相关代码
线程池
https://git.code.tencent.com/ThisisGame/DreamEngine/blob/6247d333dab05f58c92383c67756709df841703a/Engine/Src/Tools/ThreadPool.hSkinnedMeshRenderer
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