预备知识可参考我整理的博客
- Windows编程之线程:https://www.cnblogs.com/ZhuSenlin/p/16662075.html
- Windows编程之线程同步:https://www.cnblogs.com/ZhuSenlin/p/16663055.html
代码结构
一个简单的线程库需要实现的功能主要有:
- 创建和结束一个线程
- 设置线程的优先级
- 提供一些线程调度的接口
- 查询线程的状态
- 退出一个线程
- 多线程运行时同步的解决方案
- 线程池(非必要):多用于网络请求、单一且快速能解决的任务。
利用C++类的生命周期,,我们可以实现一个线程的创建放在构造函数上,结束放在析构函数上。当想要实现一个特殊线程时,就采用继承的方式拓展这个线程类。
- 一个基本的类框架如下
//Thread.h 线程基类 class Thread { public: Thread() { //Create a thread //函数入口为:ThreadMain((void)this); } ~Thread() { //Terminate a thread } protected: //线程执行的纯虚函数,子类重写这个函数来说明线程需要执行的任务 virtual int Run()=0; private: //此函数会调用(Thread*)param->Run(); static unsigned _stdcall ThreadMain(void* param); } //ThreadSync.h 线程同步的方式 //1.原子操作函数 //2.关键段 //3.事件内核对象 //4.可等待的计时器内核对象 //5.信号量内核对象 //6.互斥量内核对象
线程同步的实现
首先我们要明确的一点是:用户方式的线程同步较为简单且独立,仅作稍微的封装为引擎统一风格的代码即可;而对象内核的同步方式是比较统一的,它们的阻塞与恢复是由等待函数(WaitForSingleObject或WaitForMultipleObjects)来实现的,引起它们其实可以统一为一种类型。
原子函数与关键段
用户方式的线程同步比较简单,Windows API也给的比较清楚,下面是相关的代码展示。
Interlocked家族函数的封装
- 代码
//原子操作:++ //*pValue++ FORCEINLINE void TInterlockedIncrement(unsigned long long* pValue) { ::InterlockedIncrement(pValue); //原子操作:-- //*pValue-- FORCEINLINE void TInterlockedDecrement(unsigned long long* pValue) { ::InterlockedDecrement(pValue); //原子操作:+= //*added+=addNum FORCEINLINE void TInterlockedExchangeAdd(PLONG added, LONG addNum) { ::InterlockedExchangeAdd(added, addNum); //原子操作:-= //*added-=addNum FORCEINLINE void TInterlockedExchangeSub(PULONG subed, LONG subNum) { ::InterlockedExchangeSubtract(subed, subNum); //原子操作:= //target=lvalue; FORCEINLINE LONG TInterlockedExchange(PLONG target, LONG value) { return ::InterlockedExchange(target, value); //原子操作:= //pTarget=&pVal FORCEINLINE PVOID TInterlockedExchangePointer(PVOID* pTarget, PVOID pVal) { return ::InterlockedExchangePointer(pTarget, pVal); //原子操作: //if(*pDest==compare) // *pDest=value; FORCEINLINE LONG TInterlockedCompareExchange(PLONG pDest, LONG value, LONG compare) { return ::InterlockedCompareExchange(pDest, value, compare); //原子操作: //if(*pDest==pCompare) // pDest=&value; FORCEINLINE PVOID TInterlockedCompareExchangePointer(PVOID* ppDest, PVOID value, PVOIpCompare) { //如果ppvDestination和pvCompare相同,则执行ppvDestination=pvExchange,否则不变 return ::InterlockedCompareExchangePointer(ppDest, value, pCompare); }
其实上面的代码就是将Windows API 修改了函数命名。我个人认为,这种写代码的方式是有益处。因为线程库这一块的代码是较为底层的部分,如果上层直接调用API,一旦遇到了Windows API过时等问题导致的实现方式要修改的情况,你就需要一个项目一个项目的去修改名称,这是不严谨的。代码的底层要尽可能地隐藏代码的实现部分,仅提供功能接口。
- 用例:两个线程同时对一个变量进行++操作
int m_gCount=0; //全局变量 class Thread1 : public Thread { //... virtual int Run() { TInterlockedIncrement(&((unsigned long long)m_gCount)); } } class Thread2 : public Thread { //... virtual int Run() { TInterlockedIncrement(&((unsigned long long)m_gCount)); } }
关键段的封装
- 代码
//Defines [.h] //----------------------------------------------------------------------- class TURBO_CORE_API CriticalSection { public: CriticalSection(); //初始化关键段变量 ~CriticalSection(); //删除关键段变量 //挂起式关键段访问:即若有其他线程访问时,则调用处会挂起等待 inline void Lock(); //结束访问关键段 inline void Unlock(); //非挂起式关键段访问 //若有其他线程访问此关键段,则返回FALSE。可以访问则放回TRUE inline bool TryLock(); private: CRITICAL_SECTION m_cs; } //implement[.cpp] //----------------------------------------------------------------------- TurboEngine::Core::CriticalSection::CriticalSection() { ::InitializeCriticalSection(&m_cs); } TurboEngine::Core::CriticalSection::~CriticalSection() { ::DeleteCriticalSection(&m_cs); } inline void TurboEngine::Core::CriticalSection::Lock() { ::EnterCriticalSection(&m_cs); } inline void TurboEngine::Core::CriticalSection::Unlock() { ::LeaveCriticalSection(&m_cs); } inline bool TurboEngine::Core::CriticalSection::TryLock() { return ::TryEnterCriticalSection(&m_cs); } inline void TurboEngine::Core::CriticalSection::SetSpinCount(DWORD dwSpinCount) { ::SetCriticalSectionSpinCount(&m_cs, dwSpinCount); }
- 用例:两个线程同时对一个变量进行++操
CriticalSection m_cs; int m_gCount=0; class Thread1 : public Thread { //... virtual int Run() { m_cs.Lock(); //若有其他线程访问m_gCount则线程挂起等待 m_gCount++; m_cs.Unlock(); } } class Thread2 : public Thread { //... virtual int Run() { if(m_cs.TryLock()) { m_gCount++; m_cs.Unlock(); } } }
内核对象的同步方式
代码结构

- SyncKernelObject
- SyncTrigger
- SyncTimer
- SyncSemaphore
- SyncMutex
SyncKernelObject基类
基类理所应当的封装了线程同步内核对象所需要的一些变量和函数。我们都知道,对于所有的同步内核对象,实现同步都依赖与Wait函数,因此,我也把Wait函数封装在了父类上。基类的代码如下所示:
//Defines [.h] //----------------------------------------------------------------------------------------------------------------------- class TURBO_CORE_API SyncKernelObject { public: //等待得状态 enum WaitState : DWORD { Abandoned = WAIT_ABANDONED, //占用此内核对象的线程突然被终止时,其他等待的线程中的其中一个会收到WAIT_ABANDONED Active = WAIT_OBJECT_0, //等待的对象被触发 TimeOut = WAIT_TIMEOUT, //等待超时 Failded = WAIT_FAILED, //给WaitForSingleObject传入了无效参数 Null = Failded - 1 //占用了一个似乎没有相关值得变量表示句柄为NULL(Failed-1) }; public: SyncKernelObject(PSECURITY_ATTRIBUTES psa = NULL, LPCWSTR objName = NULL); ~SyncKernelObject(); public: //获取内核对象的句柄 inline HANDLE GetHandle() { return m_KernelObjHandle; } //获取内核对象的名称 inline const LPCWSTR GetName() { return m_Name; } //获取内核对象的安全性结构体 inline PSECURITY_ATTRIBUTES GetPsa() { return m_psa; } //(静态函数)多个内核对象的等待函数 inline static DWORD Waits(DWORD objCount, CONST HANDLE* pObjects, BOOL waitAll, DWORDwaitMilliSeconds) { return WaitForMultipleObjects(objCount, pObjects, waitAll, waitMilliSeconds); } protected: //自身相关的等待函数 WaitState Wait(DWORD milliSeconds); protected: HANDLE m_KernelObjHandle; //内核对象句柄 LPCWSTR m_Name; //内核对象名称,默认为NULL PSECURITY_ATTRIBUTES m_psa; //安全性相关得结构体,通常为NULL }
SyncTrigger
事件内核对象。我更愿意称它为触发器、开关。作为一个触发器,它存在激活与非激活两种状态,我们可以利用这种状态灵活的控制线程同步问题。
//Defines [.h] class TURBO_CORE_API SyncTrigger : public SyncKernelObject { public: SyncTrigger(bool bManual, bool isInitialActive, LPCWSTR objName = NULLPSECURITY_ATTRIBUTES psa = NULL); ~SyncTrigger() //时间内核对象的等待函数(调用父类的Wait函数) WaitState CheckWait(DWORD waitMilliSeconds) //当前是否为激活状态 bool IsTrigger(); //设置当前状态为激活 bool SetActive(); //设置当前状态为未激活 bool SetInactive(); };
- 函数解析:
- SyncTrigger:唯一构造函数。bManual为是否是手动重置,isInitialActive为初始激活的状态。
- CheckWait:常规的内核对象Wait函数
- IsTrigger:等待时间为0的Wait函数,用于获取当前Trigger的触发状态
- SetActive:将Trigger设置为触发状态
- SetInactive:Trigger设置为非触发状态
- 用例
//利用触发器作为线程退出的标记(可以避免强行终止线程的操作) SyncTrigger m_Trigger(true,false); //手动重置、初始状态为非激活的触发器 //某个线程的入口函数 virtual DWORD WINAPI Run() { //若此触发器未激活,则持续循环 while(!m_Trigger.IsTrigger()) { //TO-DO } //退出线程 return 0; } //当需要退出该线程时,可以调用如下,线程可跳出执行的循环 m_Trigger.SetActive(); //激活此触发器
SyncTimer
计时器内核对象顾名思义,就是和时间相关的控制器。当SyncTimer的内核对象设置为自动重置时,此计时器可以周期性的设置内核对象为激活状态,这就是SyncTimer的主要功能。类的属性和函数如下所示:
class TURBO_CORE_API SyncTimer : public SyncKernelObject { public: SyncTimer(bool bManual, LPCWSTR objName = NULL, PSECURITY_ATTRIBUTES psa = NULL); ~SyncTimer() //内核对象的等待函数(调用父类的Wait函数) WaitState CheckWait(DWORD waitMilliSeconds); //当前是否为激活状态 bool IsTrigger(); //开始计时器 bool StartTimer(const LARGE_INTEGER* startTime, LONG circleMilliSeconds); //取消计时器 bool CancelTimer(); };
- 函数简析
- SyncTimer:唯一构造函数。bManual为是否是手动重置
- CheckWait:常规的内核对象Wait函数
- IsTrigger:等待时间为0的Wait函数,用于获取当前Trigger的触发状态
- StartTimer:startTime为起始的事件,具体如何赋值可以参考MSDN文档;circleMilliSeconds为周期触发的时 长(毫秒)。注意:此参数只有在内核对象为自动重置模式才有意义。
- CancelTimer:取消开始的计时器
- 用例
//每秒钟SyncTimer激活一次的程序代码 SyncTimer m_gSyncTimer(false); //自动重置的计时器内核对象 //某个线程的入口函数 virtual DWORD WINAPI Run() { //若此触发器未激活,则持续循环 while(!m_Trigger.IsTrigger()) { //使用计时器 if (m_gSyncTimer.IsTrigger()) cout << "SyncTimer激发一次n"; } //退出线程 return 0; } //注意startTime的参数如何编写: LARGE_INTEGER liDueTime; liDueTime.QuadPart = 0; m_gSyncTimer.StartTimer(&liDueTime, 1000); //设定计时器为1S钟激活一次
startTime:如果值是正的,代表一个特定的时刻。如果值是负的,代表以100纳秒为单位的相对时间
SyncSemaphore
class TURBO_CORE_API SyncSemaphore : public SyncKernelObject { public: SyncSemaphore(LONG initialCount, LONG maximumCount, LPCWSTR objName = NULLPSECURITY_ATTRIBUTES psa = NULL); ~SyncSemaphore(); //申请使用一个资源(此时的引用计数将会减1) WaitState Lock(DWORD dwMilliseconds); //释放一个资源 //releaseCount:释放的数量 //oldResCount:未释放前资源的数量 bool Unlock(DWORD releaseCount = 1, LPLONG oldResCount = NULL); };
- 函数简析
- SyncSemaphore: 唯一构造函数。initialCount:资源创建后立即占用的数量;maximumCount内核对象管理资源的最大数量
- Lock:申请使用一个资源
- Unlock:释放资源
SyncMutex
//互斥内核对象 //可以理解为内核对象版的关键段 class TURBO_CORE_API SyncMutex : public SyncKernelObject { public: SyncMutex(bool initialOccupied, LPCWSTR objName = NULL, PSECURITY_ATTRIBUTES psa NULL); ~SyncMutex(); //挂起式申请访问(若申请访问的变量被占用时则线程挂起) void Lock(); //结束访问 bool Unlock(); //非挂起式访问 //若有其他线程访问此关键段,则返回FALSE。可以访问则放回TRUE bool TryLock(DWORD milliSeconds=0); };
- 函数简析(略),和关键段功能相同
- 用例
//Run1()和Run2()不会发生访问冲突而引发未知结果 SyncMutex m_gMutex(false); int m_gSyncCounter1=0; //某个线程的入口函数 virtual DWORD WINAPI Run1() { //若此触发器未激活,则持续循环 while(!m_Trigger.IsTrigger()) { if (m_gMutex.TryLock()) { cout << "线程[" << GetThreadId() << "]完成一次累加:[" << m_gSyncCounter1 << "]" << "n"; m_gMutex.Unlock(); } } } //某个线程的入口函数 virtual DWORD WINAPI Run2() { //若此触发器未激活,则持续循环 while(!m_Trigger.IsTrigger()) { if (m_gMutex.TryLock()) { cout << "线程[" << GetThreadId() << "]完成一次累加:[" << m_gSyncCounter1 << "]" << "n"; m_gMutex.Unlock(); } } }
线程类的实现
上一节我们讲了线程同步的方式,通过编写的线程同步代码。我们使用多线程的时候可以正确的访问一些公共变量。那么关键的线程类我们该如何实现呢。自己对线程理解如下图所示。

相关基类的定义代码如下:
//引擎线程基类 class TURBO_CORE_API Thread { public: enum class PriorityLevel : int { TimeCritical = THREAD_PRIORITY_TIME_CRITICAL, Highest = THREAD_PRIORITY_HIGHEST, AboveNormal = THREAD_PRIORITY_ABOVE_NORMAL, Normal = THREAD_PRIORITY_NORMAL, BelowNormal = THREAD_PRIORITY_BELOW_NORMAL, Lowest = THREAD_PRIORITY_LOWEST, Idle = THREAD_PRIORITY_IDLE }; enum class ThreadState { Initialized, Running, Suspend, Stop, }; public: //线程构造函数 //priorityLevel:线程优先级,默认为<normal> //stackSize:线程的堆栈大小,默认为<0> Thread(PriorityLevel priorityLevel = PriorityLevel::Normal, unsigned int stackSize = 0); ~Thread(); //开启线程 void Start(); //挂起线程 //return->返回挂起前的挂起计数 int Suspend(); //恢复线程。 //[注意,恢复一次不一定会立即执行] //return->返回恢复前的挂起系数 int Resume(); //终止线程 bool Stop(); //是否允许动态提升优先级 //Notes:在当前优先级的范围内各个切片时间上下浮动,但不会跳到下一个优先级 //当前的优先级是一个优先级范围,而不是具体的等级 bool IsAllowDynamicPriority(); //启用or禁止动态提升优先级 bool SetPriorityBoost(bool bActive); //设置线程优先级 bool SetPriority(PriorityLevel priority); //当前线程的优先级 PriorityLevel GetCurrentPriority(); //线程是否存在 bool IsAlive(); //当前线程的状态 ThreadState GetCurrentState(); //获取线程Id DWORD GetThreadId(); //线程名称 virtual const CHAR* ThreadName() = 0; protected: //线程的主逻辑函数 virtual DWORD WINAPI Run() = 0; //线程函数入口 static unsigned _stdcall ThreadEnterProc(void* param); protected: HANDLE m_ThreadHandle = NULL; //线程句柄 unsigned int m_ThreadStackSize = 0; //线程堆栈大小 ThreadState m_CurrentState; //当前线程的状态 PriorityLevel m_CurrentPriority; //当前线程的优先级 SyncTrigger m_TerminateThreadTrigger; //终止线程的触发器 }; }
具体如何是实现,如果说熟悉Windows提供的线程API,我想很快就能实现。那么如何开启一个线程呢。既然上面的基类基本实现了对一个线程创建、销毁、调度的函数。那么每个线程的差异点应该在两个虚函数上。
//定义线程名称的位置 virtual const CHAR* ThreadName() = 0; //线程入口函数的实现代码放置的位置 virtual DWORD WINAPI Run() = 0;
- 用例:定义一个渲染线程并开启
class RenderThread : public Thread { public: virtual const CHAR* ThreadName() { return "RenderThread"; } protected: virtual DWORD WINAPI Run() { //StartRender while(!gameStop) { RenderOpaque(); RenderTransparent(); //... } } } //开启渲染线程 RenderThread m_gRenderThread; m_gRenderThread.Start();
结语
上面的线程类和线程同步类共同构成了引擎简单的线程库。当然,真正可用的游戏引擎,其线程库不可能这么简单,但是,对于目前而言,这也足够使用。
碍于篇幅,很多代码仅提供了类的定义,关于类的实现,请参考Github上的项目。