• 引擎之旅 Chapter.2 线程库


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

    代码结构

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

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

    利用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();
            }
        }
    }
    
    

    内核对象的同步方式

    代码结构

    image

    • 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:线程优先级,默认为
    			//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上的项目。

  • 相关阅读:
    Ubuntu Pycharm安装
    Python机器学习 | AI芯片调研
    计算机基础知识——字,字节,进制,short,byte等
    Spring 随笔 ioc/di 3-配置注入
    echarts+vue实现柱状图分页显示
    微信第三方sdk获取openid 报错Caused by: java.lang.NoClassDefFoundError
    接口测试——接口协议抓包分析与mock_L2
    神经网络芯片的单片机,什么是神经网络芯片
    Elasticsearch搜索引擎:ES的segment段合并原理
    mysql存储过程与函数
  • 原文地址:https://www.cnblogs.com/ZhuSenlin/p/16668253.html