热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

引擎之旅Chapter.2线程库

预备知识可参考我整理的博客Windows编程之线程:https:www.cnblogs.comZhuSenlinp16662075.htmlWindows编程之线程同步:https

预备知识可参考我整理的博客

Windows编程之线程:https://www.cnblogs.com/ZhuSenlin/p/16662075.html

Windows编程之线程同步:https://www.cnblogs.com/ZhuSenlin/p/16663055.html

目录

代码结构

线程同步的实现

原子函数与关键段

Interlocked家族函数的封装

关键段的封装

内核对象的同步方式

代码结构

SyncKernelObject基类

SyncTrigger

SyncTimer

SyncSemaphore

SyncMutex

线程类的实现

结语


代码结构

一个简单的线程库需要实现的功能主要有:

创建和结束一个线程

设置线程的优先级

提供一些线程调度的接口

查询线程的状态

退出一个线程

多线程运行时同步的解决方案

线程池(非必要):多用于网络请求、单一且快速能解决的任务。

利用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 <<"线程[" <
m_gMutex.Unlock();

}

}

}
//某个线程的入口函数

virtual DWORD WINAPI Run2()

{

//若此触发器未激活,则持续循环

while(!m_Trigger.IsTrigger())

{

if (m_gMutex.TryLock())

{

cout <<"线程[" <
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:线程优先级,默认为

//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上的项目。


引擎之旅 Chapter.2 线程库的相关教程结束。



推荐阅读
  • Java学习笔记之面向对象编程(OOP)
    本文介绍了Java学习笔记中的面向对象编程(OOP)内容,包括OOP的三大特性(封装、继承、多态)和五大原则(单一职责原则、开放封闭原则、里式替换原则、依赖倒置原则)。通过学习OOP,可以提高代码复用性、拓展性和安全性。 ... [详细]
  • 如何在服务器主机上实现文件共享的方法和工具
    本文介绍了在服务器主机上实现文件共享的方法和工具,包括Linux主机和Windows主机的文件传输方式,Web运维和FTP/SFTP客户端运维两种方式,以及使用WinSCP工具将文件上传至Linux云服务器的操作方法。此外,还介绍了在迁移过程中需要安装迁移Agent并输入目的端服务器所在华为云的AK/SK,以及主机迁移服务会收集的源端服务器信息。 ... [详细]
  • 本文介绍了设计师伊振华受邀参与沈阳市智慧城市运行管理中心项目的整体设计,并以数字赋能和创新驱动高质量发展的理念,建设了集成、智慧、高效的一体化城市综合管理平台,促进了城市的数字化转型。该中心被称为当代城市的智能心脏,为沈阳市的智慧城市建设做出了重要贡献。 ... [详细]
  • 向QTextEdit拖放文件的方法及实现步骤
    本文介绍了在使用QTextEdit时如何实现拖放文件的功能,包括相关的方法和实现步骤。通过重写dragEnterEvent和dropEvent函数,并结合QMimeData和QUrl等类,可以轻松实现向QTextEdit拖放文件的功能。详细的代码实现和说明可以参考本文提供的示例代码。 ... [详细]
  • Java序列化对象传给PHP的方法及原理解析
    本文介绍了Java序列化对象传给PHP的方法及原理,包括Java对象传递的方式、序列化的方式、PHP中的序列化用法介绍、Java是否能反序列化PHP的数据、Java序列化的原理以及解决Java序列化中的问题。同时还解释了序列化的概念和作用,以及代码执行序列化所需要的权限。最后指出,序列化会将对象实例的所有字段都进行序列化,使得数据能够被表示为实例的序列化数据,但只有能够解释该格式的代码才能够确定数据的内容。 ... [详细]
  • 本文讨论了在Windows 8上安装gvim中插件时出现的错误加载问题。作者将EasyMotion插件放在了正确的位置,但加载时却出现了错误。作者提供了下载链接和之前放置插件的位置,并列出了出现的错误信息。 ... [详细]
  • Android Studio Bumblebee | 2021.1.1(大黄蜂版本使用介绍)
    本文介绍了Android Studio Bumblebee | 2021.1.1(大黄蜂版本)的使用方法和相关知识,包括Gradle的介绍、设备管理器的配置、无线调试、新版本问题等内容。同时还提供了更新版本的下载地址和启动页面截图。 ... [详细]
  • 知识图谱——机器大脑中的知识库
    本文介绍了知识图谱在机器大脑中的应用,以及搜索引擎在知识图谱方面的发展。以谷歌知识图谱为例,说明了知识图谱的智能化特点。通过搜索引擎用户可以获取更加智能化的答案,如搜索关键词"Marie Curie",会得到居里夫人的详细信息以及与之相关的历史人物。知识图谱的出现引起了搜索引擎行业的变革,不仅美国的微软必应,中国的百度、搜狗等搜索引擎公司也纷纷推出了自己的知识图谱。 ... [详细]
  • CF:3D City Model(小思维)问题解析和代码实现
    本文通过解析CF:3D City Model问题,介绍了问题的背景和要求,并给出了相应的代码实现。该问题涉及到在一个矩形的网格上建造城市的情景,每个网格单元可以作为建筑的基础,建筑由多个立方体叠加而成。文章详细讲解了问题的解决思路,并给出了相应的代码实现供读者参考。 ... [详细]
  • C++字符字符串处理及字符集编码方案
    本文介绍了C++中字符字符串处理的问题,并详细解释了字符集编码方案,包括UNICODE、Windows apps采用的UTF-16编码、ASCII、SBCS和DBCS编码方案。同时说明了ANSI C标准和Windows中的字符/字符串数据类型实现。文章还提到了在编译时需要定义UNICODE宏以支持unicode编码,否则将使用windows code page编译。最后,给出了相关的头文件和数据类型定义。 ... [详细]
  • Go Cobra命令行工具入门教程
    本文介绍了Go语言实现的命令行工具Cobra的基本概念、安装方法和入门实践。Cobra被广泛应用于各种项目中,如Kubernetes、Hugo和Github CLI等。通过使用Cobra,我们可以快速创建命令行工具,适用于写测试脚本和各种服务的Admin CLI。文章还通过一个简单的demo演示了Cobra的使用方法。 ... [详细]
  • 本文讨论了clone的fork与pthread_create创建线程的不同之处。进程是一个指令执行流及其执行环境,其执行环境是一个系统资源的集合。在调用系统调用fork创建一个进程时,子进程只是完全复制父进程的资源,这样得到的子进程独立于父进程,具有良好的并发性。但是二者之间的通讯需要通过专门的通讯机制,另外通过fork创建子进程系统开销很大。因此,在某些情况下,使用clone或pthread_create创建线程可能更加高效。 ... [详细]
  • 本文介绍了如何使用Express App提供静态文件,同时提到了一些不需要使用的文件,如package.json和/.ssh/known_hosts,并解释了为什么app.get('*')无法捕获所有请求以及为什么app.use(express.static(__dirname))可能会提供不需要的文件。 ... [详细]
  • JDK源码学习之HashTable(附带面试题)的学习笔记
    本文介绍了JDK源码学习之HashTable(附带面试题)的学习笔记,包括HashTable的定义、数据类型、与HashMap的关系和区别。文章提供了干货,并附带了其他相关主题的学习笔记。 ... [详细]
  • Whatsthedifferencebetweento_aandto_ary?to_a和to_ary有什么区别? ... [详细]
author-avatar
mobiledu2402851373
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有