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原理及实现的主要内容,如果未能解决你的问题,请参考以下文章

python multithread task_done

MultiThread(VS2013 MFC多线程-含源码-含个人逐步实现文档)

多线程--MultiThread

Multithread 什么时候处理多线程,有几种方式,优缺点?

MultiThread SkinnedMeshRenderer原理及实现

MultiThread SkinnedMeshRenderer原理及实现