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

代码结构

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

  • 创建和结束一个线程
  • 设置线程的优先级
  • 提供一些线程调度的接口
  • 查询线程的状态
  • 退出一个线程
  • 多线程运行时同步的解决方案
  • 线程池(非必要):多用于网络请求、单一且快速能解决的任务。

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

引擎之旅 Chapter.2 线程库的更多相关文章

  1. 引擎之旅 Chapter.4 日志系统

    关于近段时间为何没有更新的解释:Find a new job. 目录 引言 日志语句的分类 控制台窗体 和 VSOutput Tab的日志打印 存储至特定的文件中 展示堆栈信息 引言 一般来说,一个优 ...

  2. 引擎之旅 Chapter.1 高分辨率时钟

    目录 游戏中的时间线 真实时间线 游戏时间线 全局时钟的实现方式 我们如何理解时间.在现实生活中,时间就是一个有方向的直线.从一个无穷远到另一个无穷远.用数学去抽象地思考,它就是一个从无穷小到无穷大的 ...

  3. Linux posix线程库总结

    由于历史原因,2.5.x以前的linux对pthreads没有提供内核级的支持,所以在linux上的pthreads实现只能采用n:1的方式,也称为库实现. 线程的实现,经历了如下发展阶段: Linu ...

  4. 【Jetlang】一个高性能的Java线程库

    actor  Jetlang 提供了一个高性能的Java线程库,该库是 JDK 1.5 中的 java.util.concurrent 包的补充,可用于基于并发消息机制的应用. .net的MS CCR ...

  5. Boost线程库学习笔记

    一.创建一个线程 创建线程 boost::thread myThread(threadFun); 需要注意的是:参数可以是函数对象或者函数指针.并且这个函数无参数,并返回void类型. 当一个thre ...

  6. Arduino线程库ProtoThreads

    参考: Arduino线程库ProtoThreads 一个“蝇量级” C 语言协程库

  7. 130行C语言实现个用户态线程库——ezthread

    准确的说是除掉头文件,测试代码和非关键的纯算法代码(只有双向环形链表的ADT),核心代码只有130行左右,已经是蝇量级的用户态线程库了.把这个库取名为ezthread,意思是,这太easy了,人人都可 ...

  8. C#多线程之旅(7)——终止线程

    先交代下背景,写<C#多线程之旅>这个系列文章主要是因为以下几个原因:1.多线程在C/S和B/S架构中用得是非常多的;2.而且多线程的使用是非常复杂的,如果没有用好,容易造成很多问题.   ...

  9. OS之进程管理---多线程模型和线程库(POSIX PTread)

    多线程简介 线程是CPU使用的基本单元,包括线程ID,程序计数器.寄存器组.各自的堆栈等,在相同线程组中,所有线程共享进程代码段,数据段和其他系统资源. 传统的的单线程模式是每一个进程只能单个控制线程 ...

随机推荐

  1. linux web漏洞扫描arachni

    1. 下载arachni https://www.arachni-scanner.com/download/下载Linux x86 64bit 2. 上次解压直接使用 tar xzf arachni- ...

  2. 超 Nice 的表格响应式布局小技巧

    今天,遇到了一个很有意思的问题,一名群友问我,仅仅使用 CSS,能否实现这样一种响应式的布局效果: 简单解析一下效果: 在屏幕视口较为宽时,表现为一个整体 Table 的样式 而当屏幕视口宽度较小时, ...

  3. 用 PyQt5 快速构建一个简单的 GUI 应用

    1. 介绍 Python GUI 常用的 3 种框架是:Tkinter.wxpython.PyQt5 PyQt5 基于 Qt,是 Python 和 Qt 的结合体,可以用 Python 语言编写跨平台 ...

  4. 10道不得不会的JavaEE面试题

    10道不得不会的 JavaEE 面试题 我是 JavaPub,专注于面试.副业,技术人的成长记录. 以下是 JavaEE 面试题,相信大家都会有种及眼熟又陌生的感觉.看过可能在短暂的面试后又马上忘记了 ...

  5. 「笔记」折半搜索(Meet in the Middle)

    思想 先搜索前一半的状态,再搜索后一半的状态,再记录两边状态相结合的答案. 暴力搜索的时间复杂度通常是 \(O(2^{n})\) 级别的.但折半搜索可以将时间复杂度降到 \(O(2 \times 2^ ...

  6. 隐私计算FATE-多分类神经网络算法测试

    一.说明 本文分享基于 Fate 使用 横向联邦 神经网络算法 对 多分类 的数据进行 模型训练,并使用该模型对数据进行 多分类预测. 二分类算法:是指待预测的 label 标签的取值只有两种:直白来 ...

  7. String类常用的API

    String类常用的API 字符串内容的比较: 注意: 不能使用 == 去比较两个字符串的内容.原理:比较的是字符串的地址. (如果两个字符串都是使用""进行赋值,那么他们都是放在 ...

  8. Hadoop集群搭建(完全分布式版本) VMWARE虚拟机

    Hadoop集群搭建(完全分布式版本) VMWARE虚拟机 一.准备工作 三台虚拟机:master.node1.node2 时间同步 ntpdate ntp.aliyun.com 调整时区 cp /u ...

  9. 两个比较好用的JS方法,用来处理树形结构!

    一.平级结构转树形结构 /** * 平级结构转树形结构 * * 示例:const jsonDataTree = listTransToTreeData(jsonData, 'id', 'pid', ' ...

  10. python打开文件、文件夹窗口、终端窗口

    简介 在一些项目中,我们会需要在生成完文件后打开某些文件或者文件夹窗口,这就需要使用到内置的文件打开方式了. 打开文件或文件夹 Windows import os import subprocess ...