UE4异步编程专题 - 多线程

专题的第二篇,我们聊聊UE4中的多线程的基础设施。UE4中最基础的模型就是FRunnable和FRunnableThread,FRunnable抽象出一个可以执行在线程上的对象,而FRunnableThread是平台无关的线程对象的抽象。后面的篇幅会详细讨论这些基础设施。

1. FRunnable

UE4为我们抽象FRunnable的概念,让我们指定在线程上运行的一段逻辑过程。该过程通常是一个较为耗时的操作,例如文件IO;或者是常态为空闲等待(Idle)的循环,随时等待新执行命令到来。

FRunnable为我们提供了四个重要的接口:

class CORE_API FRunnable
{
public:// ....virtual bool Init();virtual uint32 Run() = 0;virtual void Stop() {}virtual void Exit() {}
};
  1. Init是对FRunnable对象的初始化,它是由FRunnableThread在创建线程对象后,进入线程函数的时候立即被FRunnableThread调用的函数,并不能由用户自己调用;
  2. Run是Runnable过程的入口,同样也是有FRunnableThread在Init成功后调用;
  3. Exit是Run正常退出后,由FRunnableThread调用,进行对FRunnable对象的清理工作;
  4. Stop是给用户使用的接口,当我们觉得必要时停止FRunnable.

例如一个空闲等待的FRunnable的实现:

class MyRunnable : public FRunnable
{
public:MyRunnable(): RunningFlag(false), WorkEvent(FPlatformProcess::GetSynchEventFromPool()){}~MyRunnable(){FPlatformProcess::ReturnSynchEventToPool(WorkEvent);WorkEvent = nullptr;}bool Init() override{// ..if(!WorkEvent)return false;RunningFlag.Store(true);}void Run() override{while(RunningFlag.Load()){WorkEvent->Wait(MAX_uint32);if(!RunningFlag.Load())break;// ...}}void Stop() override{if(RunningFlag.Exchange(false))WorkEvent->Trigger();}void Exit() overrdie{// ...RunningFlag.Store(false);}void Notify(){WorkEvent->Trigger();}private:TAtomic<bool>   RunningFlag;FEvent*         WorkEvent;// ...
};

原子变量RunningFlag作为Runnable对象的运行状态的标记,所以Run函数的主体就是在RunningFlag为ture的情况无限循环。WorkEvent是其他线程上执行的任务与MyRunnable交互的事件对象,通过Notify接口,可以唤醒它继续执行。MyRunnable从Wait中醒来时,还会检查一次RunningFlag,有可能是唤醒它的是Stop接口发出的事件。而Stop的实现,会判断一下标识是否Runnable已经退出,而不用再次发出事件了。

2. FRunnableThread

FRunnable需要依附与一个FRunnableThread对象,才能被执行。例如,我们如果要执行第一节的空闲等待的Runnable:

auto* my_runnable = new MyRunnable{};auto* runnable_thread = FRunnableThread::Create(my_runnable, "IdleWait");

FRunnableThread是平台无关的线程对象的抽象,它驱动着FRunnable的初始化,执行和清理,并提供了管理线程对象生命周期,线程局部存储,亲缘性和优先级等接口。

class FRunnableThread
{// ....// Tls 索引static uint32 RunnableTlsSlot;public:// 获取Tls索引static uint32 GetTlsSlot();// 平台无关的创建线程对象接口static FRunnableThread* Create(class FRunnable* InRunnable,const TCHAR* ThreadName,uint32 InStackSize,EThreadPriority InThreadPri,uint64 InThreadAffinityMask);public:// 设置线程优先级virtual void SetThreadPriority( EThreadPriority NewPriority ) = 0;// 挂起线程virtual void Suspend( bool bShouldPause = true ) = 0;// 杀死线程virtual bool Kill( bool bShouldWait = true ) = 0;// 同步等待线程退出virtual void WaitForCompletion() = 0;protected:// 平台相关的线程对象初始化过程virtual bool CreateInternal(FRunnable* InRunnable, const TCHAR* InThreadName,uint32 InStackSize,EThreadPriority InThreadPri, uint64 InThreadAffinityMask) = 0;
};

UE4已经实现了各个平台的线程对象。Win平台使用的是系统Windows的Thread API. 而其他平台是基于pthread,不同平台实现上略有不同。通过编译选项包含平台相关的头文件,并通过FPlatformProcess类型的定义来选择相应平台的实现。参见FRunnableThread::Create函数:

FRunnableThread* FRunnableThread::Create(class FRunnable* InRunnable, const TCHAR* ThreadName,uint32 InStackSize,EThreadPriority InThreadPri, uint64 InThreadAffinityMask)
{// ....NewThread = FPlatformProcess::CreateRunnableThread();if (NewThread){if (NewThread->CreateInternal(...))// .....}// ....
}

线程对象的创建,需要指定一个FRunnable对象的实例。

FPlatformProcess::CreateRunnableThread就是简单地new一个平台相关的线程对象,而真正的初始化时在FRunnableThread::CreateInternal当中完成的。线程平台相关的API差异很大,UE4的封装尽可能地让各个平台的实现略有不同。

系统API创建的线程对象,都以_ThreadProc作为入口函数。接下来是一系列的平台相关的初始化工作,例如设置栈的大小,TLS的索引,亲缘性掩码,获取平台相关的线程ID等。之后,就会进入上一节我们提及的FRunnable的初始化流程中了。一个线程创建成功的时序图如下:

Win平台的实现中,由于API的历史原因需要_ThreadProc的调用约定是STDCALL. 因此Win平台下的_ThreadProc函数,是一个转发函数,转发给了另外一个CDECL调用约定的函数FRunnableThreadWin::GuardedRun.

3. Runnable or Callable

UE4的多线程模型是Runnable和Thread,但是有不少C++库,如标准库,是Callable and Thread. 如果使用标准库的std::thread:

int main(void)
{std::thread t{ [](){ std::cout << "Hello Thread." } };t.join();return 0;
}

暂时忽略标准库thread简陋的设施,Callable和Runnable这两个模型是可以等价的,也就是他们可以相互表达。

例如我们可以用UE4的设施,实现类似std::thread的FThread(UE4已经实现了一个):

class FThread final : public FRunnable
{
public:template <typename Func, typename ... Args>explicit FThread(Func&& f, Args&& ... args): Callable(create_callable(std::forward<Func>(f), std::forward<Args>(args)...)), Thread(FRunnableThread::Create(this, "whatever")){if(!Thread)throw std::runtime_error{ "Failed to create thread!" };}void join(){Thread->WaitForCompletion();}virtual uint32 Run() override{Callable();return 0;}private:template <typename Func, typename ... Args>static auto create_callable(Func&& f, Args&& ... args) noexcept{// 为了简单起见用了20的特性,如果是17标准以下的话,用tuple也能办到。// Eat return typereturn [func = std::forward<Func>(f), ... args = std::forward<Args>(args)](){std::invoke(func, std::forward<Args>(args...));};}private:TFunction<void()>   Callable;FRunnableThread*    Thread;
};

我们还可以用std::thread和一些封装,来实现一个的RunnableThread. 下面是一个简单的实现:

class RunnableThread
{
public:explicit RunnableThread(FRunnable* runnable): runnable_(runnable), inited_(false), init_result_(false), thread_(&RunnableThread::Run, this){std::unique_lock<std::mutex> lock{ mutex_ };cv_.wait(lock, [this](){ return inited_; });}protected:void Run(){auto result = runnable_->Init();{std::unique_lock<std::mutex> lock{ mutex_ };inited_ = true;init_result_ = result;}cv_.notify_one();if(result){runnable_->Run();runnable_->Exit();}}private:FRunnable*              runnable_;bool                    inited_;bool                    init_result_;std::thread             thread_;std::mutex              mutex_;std::condition_variable cv_;
};

虽然笔者不喜欢面向对象的设计(OOD),但UE4的FRunnable和FRunnaableThread实现得确实挺不错。没有很重的框架束缚,并且FRunnable也有着跟callable一样的表达能力,并且FRunnableThread封装了各个平台线程库几乎所有的功能特性。总体上来说,比标准库的thread设施更齐全。

4. 小结

UE4中的多线程模型用一句话概括为: A FRunnable runs on a FRunnableThread.

FRunnable是逻辑上的可执行对象的概念的抽象。对于一个具体的可执行对象的实现,用户需要实现Init和Exit接口,对Runnable需要的系统资源进行申请和释放;用户需要实现Run来控制Runnable的执行流程,并在需要的情况下实现Stop接口,来控制Runnable的退出。

FRunnableThread是UE4提供的平台无关的线程对象的抽象,并提供了控制线程对象生命周期和状态的接口。UE4实现了常见所有平台的线程对象,实现细节对用户透明。

除此之外,本文还讨论了Runnable与Callable两种模型,并且它们具有相同的表达能力。

这个系列的下一篇,将会讨论FQueuedThreadPool. 它是由FRunnable及FRunnableThread组合实现的,用于执行任务队列的线程池。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/432180.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

坑爹的UICollectionView

最近用UICoolectionView的时候遇到一个很DT的问题&#xff0c;我往VC里加12个视图&#xff0c;结果显示成这样&#xff08;右边是期待的样子&#xff09;&#xff1a; 研究了一下午&#xff0c;终于发现了问题&#xff1a; interface FpLabelCell : UICollectionViewCellproper…

UE4异步编程专题 - TFunction

0. 关于这个专题 游戏要给用户良好的体验&#xff0c;都会尽可能的保证60帧或者更高的fps。一帧留给引擎的时间也不过16ms的时长&#xff0c;再除去渲染时间&#xff0c;留给引擎时间连10ms都不到&#xff0c;能做的事情是极其有限的。同步模式执行耗时的任务&#xff0c;时长…

UE4高级功能--初探超大无缝地图的实现LevelStream

LevelStream 实现超大无缝地图--官方文档学习 The Level Streaming feature makes it possible to load and unload map files into memory as well as toggle their visibility all during play. This makes it possible to have worlds broken up into smaller chunks so th…

inside uboot (二) 启动流程

1. S3C6410 启动流程 1). 6410上电后&#xff0c;首先执行片内iROM的程序&#xff08;BL0&#xff09;&#xff0c;初始化时钟和看门狗等外围器件。 2). 然后把flash中头4K&#xff08;BL1&#xff09;的内容加载到片内的SRAM中执行。 3). 在SRAM中执行的BL1&#xff0c;初始…

你理解我的意思么?

在最近一次的电话会议里, 某leader前后说了十来句, "你理解我的意思么?". 说实话, 有些我都没理解, 不过我听到的大家的答复都是"理解!", "明白!". Leader问这样的问题, 期望是得到对方给你反馈, 结果大部分人不会直接对上级说, "是的,我不…

inside uboot (三) 异常向量表

1. 异常向量表概述 从上面的地址映射来看&#xff0c;中断向量表的地址为0xD0037400&#xff0c;因此如果我们想在SRAM中&#xff0c;也就是BL1中处理异常的话&#xff0c; 就需要把我们的异常向量表拷贝到这个地址上。或者我们可以在链接脚本中直接指定代码的地址。 如果在主…

有关GNU GCC的基本内容整理

一、GCC简介 GCC&#xff08;GNU Compiler Collection&#xff0c;GNU编译器集合&#xff09;是一套由GNU工程开发的支持多种编程语言的编译器。GCC是自由软件发展过程中的著名例子&#xff0c;由自由软件基金会 以GPL协议发布。当年Richard Stallman 刚开始写作 GCC 的时候&am…

[UE4]C++静态加载问题:ConstructorHelpers::FClassFinder()和FObjectFinder()

相关内容&#xff1a; C实现动态加载的问题&#xff1a;LoadClass<T>()和LoadObject<T>() http://aigo.iteye.com/blog/2281558C实现动态加载UObject&#xff1a;StaticLoadObject()&#xff1b;以Texture和Material为例 http://aigo.iteye.com/blog/2268056 这…

inside uboot (五) DRAM的构成

DRAM(Dynamic Random Access Memory)&#xff0c;即动态随机存取存储器. 1. Storage Cell 如上图&#xff0c;一个DRAM的基本存储单元由4个部分组成。 Storage Capacitor&#xff0c;即存储电容&#xff0c;它通过存储在其中的电荷的多和少&#xff0c;或者说电容两端电压差的…

使用json-lib进行Java和JSON之间的转换

转自http://www.cnblogs.com/mailingfeng/archive/2012/01/18/2325707.html 1. json-lib是一个java类库&#xff0c;提供将Java对象&#xff0c;包括beans, maps, collections, java arrays and XML等转换成JSON&#xff0c;或者反向转换的功能。 2. json-lib 主页 &#xff1a…

inside uboot (六) DRAM芯片的控制线及时序

Clock &#xff08;差分信号&#xff0c;CLK和nCLK&#xff09;为时钟信号 &#xff08;同一个rank共用&#xff09; CKE 时钟信号使能 &#xff08;同一个rank共用&#xff09; RAS 为行选…

MVC — 初步理解IIS工作流程

声明&#xff1a;本文只是自己的总结和积累。IIS7.x 目录 IIS流程及组成部分ASP.NET流程及组成部分IIS与ASP.NET MVC一、IIS流程及组成部分  1、Http.SYS&#xff1a;负责监听HTTP请求&#xff08;它不属于IIS范畴&#xff0c;但是和IIS联系紧密&#xff09; Http.SYS和IIS是…

卷积积分这样学!

卷积积分是一种数学运算&#xff0c;那么既然是数学运算&#xff0c;那么就得有数学的特性——定义、性质、定理。 本文将从卷积积分的理论、案例、求解方法、知识图谱四方面介绍卷积积分&#xff01; 一、【理论】卷积积分的理论 卷积积分定义&#xff1a; 卷积图解01 卷积…

世界地图并不是世界的真实样貌!甚至误差非常大

现在所用的世界地图并不是世界的真实样貌&#xff0c;甚至误差大的离谱。 地球属于三维球体&#xff0c;想完整地表现到二维平面上是不可能的&#xff0c;必须牺牲一些真实属性。因为三维降到二维肯定存在扭曲失真&#xff0c;这是维度差异所决定的&#xff0c;不可避免。 我们…

error MSB6006: cmd.exe exited with code 3

利用vs2012和qt5.5.1&#xff0c;在编译例子时发生如下错误&#xff1a; C:\Program Files (x86)\MSBuild\Microsoft.Cpp\v4.0\V110\Microsoft.CppCommon.targets(172,5): error MSB6006: “cmd.exe”已退出&#xff0c;代码为 3。 图片如下&#xff1a; 解决办法如下&#x…

数学天才用5万字让你读懂:微积分!

前面接连发了三篇麦克斯韦方程组的文章&#xff08;积分篇、微分篇和电磁波篇&#xff09;&#xff0c;从理论上来说&#xff0c;讲麦克斯韦方程组不讲微积分是不行的&#xff0c;因为人家本来就是一组积分方程和一组微分方程。 但是&#xff0c;为了让更多人&#xff0c;尤其是…

指定一个actor对pawn不可见

1. 把一个staticmesh作成一个actor 2. 给actor添加一个tag 3. 在pawn的beginPlay里面查找这个actor&#xff0c;并设置actor的owner为pawn&#xff0c;然后调用set owner no see

oracle中DECODE与CASE的用法区别

对于CASE与DECODE其实并没有太多的区别&#xff0c;他们都是用来实现逻辑判断。Oracle的DECODE函数功能很强&#xff0c;灵活运用的话可以避免多次扫描&#xff0c;从而提高查询的性能。而CASE是9i以后提供的语法&#xff0c;这个语法更加的灵活&#xff0c;提供了IF THEN ELSE…