UE4异步操作总结

虚幻本身有提供一些对异步操作的封装,这里是对这段时间接触到的“非同步”的操作进行的总结。

当前使用的UE4版本为4.18.2。

在虚幻的游戏制作中,如果不是特殊情况一般不会有用到线程的时候。但是由于实际上虚幻内部是有着许多线程机制的。

例如通常的游戏引擎中游戏线程和渲染线程都是独立的,相互之间会存在一个同步的机制。

而物理线程与游戏线程之间的同步有时候也会导致游戏的表现与预期不一致。

通常会有线程同步需求的地方是网络相关的操作,但是实际上UE4已经对网络操作进行了封装,无需关心这个问题。

而游戏线程、渲染线程、物理线程内部也都已经有了封装,对游戏逻辑的构建基本是不可见的。

但是有时候还是会遇到需要使用线程相关逻辑的,这里就是这段时间内累计的“非同步”相关逻辑的总结。

Tick

这个其实关于Tick的,虽然Actor是有默认的Tick函数的,Component与UMG也有对应的Tick机制。

但是如果是自定义的UObject或者Slate,要使用Tick机制的话就会有些麻烦。

例如,想要让自定义的Slate控件进行某种数据更新,而数据源本身并不提供通知机制的话就会有些麻烦。

虽然通过各种设计可以巧妙的绕过这个问题,但是有时候在类内部构建Tick机制才是最快速的解决方案。

TimerManager

通过使用引擎提供的定时器机制,就可以进行自定义的Tick了:

1

2

3

4

5

6

7

GetWord()->GetTimerManager().SetTimer(

  m_hTimerHandle,

  this,

  &UNetPlayManager::TimerTick,

  1.0,

  true

);

这里需要能够获得UWorld的指针,如果是自定义的类型的话,就必须想办法提供有效的UWorld指针。

FTickableGameObject

还有另一个方法,就是使用FTickableGameObject。

任何继承自FTickableGameObject的类型都会获得Tick的能力,就算不是虚幻原生的类型也可以使用,相当的便利。使用时继承自该类型,然后:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

public:

 

/** <Tick接口函数 */

virtual void Tick(float DeltaTime) override;

 

virtual bool IsTickable() const override

{

  return true;

}

 

virtual bool IsTickableWhenPaused() const override

{

  return true;

}

 

virtual TStatId GetStatId() const override

{

  RETURN_QUICK_DECLARE_CYCLE_STAT(USceneCapturer, STATGROUP_Tickables);

}

继承一下基本的函数就可以了。

线程同步

UE4对操作系统提供的线程同步相关接口进行了一定的封装。

Atomics

基本的接口可以在FPlatformAtomics找到,针对不同的平台,有不同的实现。

InterlockedAdd

InterlockedCompareExchange (-Pointer)

InterlockedDecrement (-Increment)

InterlockedExchange (-Pointer)

详细的可以参看其源码。也可以参看引擎内部的使用方式:

class FThreadSafeCounter { public: int32 Add( int32 Amount ) { return FPlatformAtomics::InterlockedAdd(&Counter, Amount); } private: volatile int32 Counter; };

1

2

3

4

5

6

7

8

9

10

class FThreadSafeCounter

{

public:

  int32 Add( int32 Amount )

  {

    return FPlatformAtomics::InterlockedAdd(&Counter, Amount);

  }

private:

  volatile int32 Counter;

};

 

FCriticalSection

用于对非线程安全的区域进行保护。

1

FCriticalSection CriticalSection;

声明之后在需要的地方进行锁操作即可,有提供作用域保护的封装:

FScopeLock Lock(&CriticalSection);

1

FScopeLock Lock(&CriticalSection);

这样就不需要自己进行Lock和Unlock了,可以有效的防止误操作导致的Bug的出现。

FSpinLock

锁操作,提供Lock,Unlock以及BlockUntilUnlocked等便利的操作。

其实内部就是对FPlatformAtomics::InterlockedExchange的一个封装。

构造函数的InSpinTimeInSeconds就是默认的锁等待间隔,默认值为0.1。

FSemaphore

这个是对信号量的封装,但是似乎不建议使用。

而且并不是对于所有的平台都有实现的,通常建议使用FEvent进行代替。

FEvent

这个相当于UE4封装的内部使用的互斥信号量机制,有基本的等待和唤醒操作。

FScopedEvent

对FEvnet的封装,在注释上能够看到使用示例:

 

1

2

3

4

5

{

        FScopedEvent MyEvent;

        SendReferenceOrPointerToSomeOtherThread(&MyEvent); // Other thread calls MyEvent->Trigger();

        // MyEvent destructor is here, we wait here.

}

这个操作就是将MyEvent发送到其他线程,直到在其他的地方MyEvnet->Trigger()被调用为止,都不会离开这个作用域继续执行。

容器

包括TArray, TMap在内的几乎大部分的容器都不是线程安全的,需要自己对同步进行管理。

当然也能看到一些线程安全的封装,例如TArrayWithThreadsafeAdd。

TLockFreePointerList

这个是一系列的类型,在Task Graph系统中被使用到。如其名称是LockFree的。

TQueue

也是LockFree的,在初始化时可以指定线程同步的类型EQueueMode,分为Mpsc(多生产者单消费者)以及Spsc(单生产者单消费者)两种模式。

只有Spsc模式是contention free的。

仔细寻找的话UE4内部有实现很多便利的类型,例如TCircularQueue这种针对双线程,一个消费一个生产的线程安全类型。

工具类

FThreadSafeCounter

就是前面例子中的线程安全的计数器。

FThreadSingleton

为每一个线程创建一个实例。

FThreadIdleStats

用于统计线程空闲状态。

异步执行

UE4中对基本的线程操作进行了一定程度的封装,使用相应的Helper就可以无需关心线程的创建这些问题。

AsyncTask

这个函数可以将一些简单的任务扔到UE4的线程池中去进行,不必关心具体的线程同步问题。

1

2

3

4

5

6

7

8

9

10

11

if(IsInGameThread())

{

  //….一些操作

}

else

{

  AsyncTask(ENamedThreads::GameThread, [=]()

  {

    //….一些操作

  });

}

其中第一个参数是发送到的线程的名称,通常一些工作线程是无法执行引擎中IsGameThread()保护或者其他隐形的游戏线程代码的,通过这个操作将其发送到游戏线程的话使用GameThread就可以了。

其实基本上的游戏逻辑中使用最多的就是这个函数了。

RHICmdList

这是一组独特的宏,用于将操作发送到渲染线程进行操作。

主要是对Texture之类的数据在GPU以及GPU相关的指令进行执行。

例如:

 

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

if (IsInRenderingThread())

{

    // Initialize the vertex factory's stream components.

    FDataType NewData;

    NewData.PositionComponent = STRUCTMEMBER_VERTEXSTREAMCOMPONENT(InVertexBuffer, FPaperSpriteVertex, Position, VET_Float3);

    NewData.TangentBasisComponents[0] = STRUCTMEMBER_VERTEXSTREAMCOMPONENT(InVertexBuffer, FPaperSpriteVertex, TangentX, VET_PackedNormal);

    NewData.TangentBasisComponents[1] = STRUCTMEMBER_VERTEXSTREAMCOMPONENT(InVertexBuffer, FPaperSpriteVertex, TangentZ, VET_PackedNormal);

    NewData.ColorComponent = STRUCTMEMBER_VERTEXSTREAMCOMPONENT(InVertexBuffer, FPaperSpriteVertex, Color, VET_Color);

    NewData.TextureCoordinates.Add(FVertexStreamComponent(InVertexBuffer, STRUCT_OFFSET(FPaperSpriteVertex, TexCoords), sizeof(FPaperSpriteVertex), VET_Float2));

    SetData(NewData);

}

else

{

    ENQUEUE_UNIQUE_RENDER_COMMAND_TWOPARAMETER(

        InitPaperSpriteVertexFactory,

        FPaperSpriteVertexFactory*, VertexFactory, this,

        const FPaperSpriteVertexBuffer*, VB, InVertexBuffer,

        {

            VertexFactory->Init(VB);

        });

}

这样就可以保证只能在渲染线程执行的代码不会被其他线程执行到。

渲染线程还有一些需要注意的是,UE4中有的代码的执行其实是在渲染线程中的,如果没有留意的话会造成隐形的线程同步问题。例如通常UMG的OnPaint。

FAsyncTask

这个是一组任务的封装类,是基本的任务单元,最简单的使用如下:

FAutoDeleteAsyncTask

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

class ExampleAutoDeleteAsyncTask : public FNonAbandonableTask

{

    friend class FAutoDeleteAsyncTask<ExampleAutoDeleteAsyncTask>;

 

    int32 ExampleData;

 

    ExampleAutoDeleteAsyncTask(int32 InExampleData)

        : ExampleData(InExampleData)

    {

        UE_LOG(LogTemp, Log, TEXT("[ExampleAutoDeleteAsyncTask] Construct()"));

    }

 

    void DoWork()

    {

        UE_LOG(LogTemp, Log, TEXT("[ExampleAutoDeleteAsyncTask] DoWork()"));

    }

 

    FORCEINLINE TStatId GetStatId() const

    {

        RETURN_QUICK_DECLARE_CYCLE_STAT(ExampleAutoDeleteAsyncTask, STATGROUP_ThreadPoolAsyncTasks);

    }

};

在完成定义后,可以有两种使用方式:

1

2

3

4

5

// 将任务扔到线程池中去执行

(new FAutoDeleteAsyncTask<ExampleAutoDeleteAsyncTask>(5))->StartBackgroundTask();

 

// 直接在当前线程执行操作

(new FAutoDeleteAsyncTask<ExampleAutoDeleteAsyncTask>(5))->StartSynchronousTask();

FAutoDeleteAsyncTask的一个优点是,在执行完成后会自动销毁,无需进行额外的关注。通常文件写入或者压缩数据之类的无须进行过程管理的操作可以交付给他执行。

FAsyncTask

这个才是本尊,由于不会自动删除,有需要进行额外操作的情况。

1

2

3

4

5

6

7

8

9

10

11

12

13

14

MyTask->StartSynchronousTask();

 

//to just do it now on this thread

//Check if the task is done :

 

if (MyTask->IsDone())

{

}

 

//Spinning on IsDone is not acceptable( see EnsureCompletion ), but it is ok to check once a frame.

//Ensure the task is done, doing the task on the current thread if it has not been started, waiting until completion in all cases.

 

MyTask->EnsureCompletion();

delete Task;

但是如果是使用StartBackgroundTask()的话依然不需要自己进行管理。

FRunnable

这个是交付给线程的执行体封装,通常用于比AsyncTask更加复杂的操作。

分为Init(), Run(), Exit()三个操作,如果Init失败就不会执行Run(),Run()执行完成就会执行Exit()。

 

1

2

3

4

5

6

7

8

9

10

11

class FRunAbleTest : public FRunnable

{

    virtual uint32 Run() override

    {

        UE_LOG(LogTemp, Log, TEXT("[FRunAbleTest] Run()"));

        FPlatformProcess::Sleep(30);

        UE_LOG(LogTemp, Log, TEXT("[FRunAbleTest] Run(): Comp"));

        return 0;

    }

 

};

通常也可以只指定Run(),然后交付给线程:

 

1

2

FRunnable* tp_Runable = new FRunAbleTest();

mp_TestThread = FRunnableThread::Create(tp_Runable, TEXT("Test_01"));

就可以了。

Async

这是另一个异步执行的宏,与AsyncTask有少许不同。

Async的简单的使用方式在注释中有提到

 

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

    // 使用全局函数

    int TestFunc()

    {

        return 123;

    }

 

    TFunction<int()> Task = TestFunc();

    auto Result = Async(EAsyncExecution::Thread, Task);

 

    // 使用lambda

    TFunction<int()> Task = []()

    {

        return 123;

    }

 

    auto Result = Async(EAsyncExecution::Thread, Task);

 

 

    // 使用inline lambda

    auto Result = Async<int>(EAsyncExecution::Thread, []() {

        return 123;

    }

第一个参数为执行的类型,TaskGraph是将其放到任务图中去执行,Thread则是在单独的线程中执行,TreadPool则是放入线程池中去执行。

这里并不能像AsyncTask一样指定目标的线程。

同时Async会返回一个TFuture<ResultType>,而ResultType则是传入的执行函数的返回值。

 

1

2

3

4

5

6

TFunction<int()> My_Task= []() {

    return 123;

};

 

auto Future = Async(EAsyncExecution::TaskGraph, My_Task);

int Result = Future.Get();

类似这样的调用即可。

总结

UE4提供的异步操作大体上分为TaskGraph和TreadPool的管理方式,通常较简单的任务交付给TaskGraph,复杂的任务交付给Thread。

对于Task,引擎会有自己的管理,将其分配给空闲的Worker Thread。同时Task之间的依赖关系也会被管理,并按照需要的顺序被执行。

其实TaskGroup和ThreadPool都是可以自己进行申请和管理的,但是并没有实际的进行研究。

因为理论上,除非有需求,应当尽量的让游戏逻辑保持简洁。再加上线程同步是要支付额外的成本的,因此,要尽量避免对异步逻辑的使用,即使使用,也要尽量的保持逻辑单纯。而且这两个系统本身是虚幻为编辑器而设计的,虽然开放给用户使用,但是就像GamePlayAbility系统一样。本身每个程序员都有自己的实现思路,也没有必要一定要使用这套系统。

毕竟游戏最终是用户体验,没有用户在意屏幕背后的逻辑实现是否”Geek”。

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

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

相关文章

JavaScript模式读书笔记 第3章 字面量和构造函数

1&#xff0c;对象字面量 -1&#xff0c;Javascript中所创建的自定义对象在任务时候都是可变的。可以从一个空对象开始&#xff0c;根据需要增加函数。对象字面量模式可以使我们在创建对象的时候向其添加函数。<script>//定义空对象var dog {};//对空对象添加方法dog.na…

如何在UE4中创建线程

FRunnable和FRunnableThread方法对于大多数问题来说无疑是一个可行的解决方案。 但是&#xff0c;在创建许多任务时&#xff0c;您可能会达到CPU可以处理的并发上限&#xff0c;此时并发线程实际上会在争用CPU时间时相互阻碍。 然后可能值得查看FQueuedThreadPool以限制任务可用…

[Leveldb源码剖析疑问]-block_builder.cc之Add函数

Add函数是给一个Data block中添加对应的key和value,函数源码如下,其中有一处不理解: L30~L34是更新last_key_的,不理解这里干嘛不直接last_key_ key.ToString(); 写成 // Update state last_key_.resize(shared); last_key_.append(key.data() shared, non_shared); assert(S…

UE4多线程任务系统详解

首先&#xff0c;了解一下该系统重要的数据类型. 1. FQueuedThreadPool&#xff1a;虚基类&#xff0c;队列线程池, FQueuedThreadPoolBase继承自FQueuedThreadPool&#xff0c; FQueuedThreadPoolBase维护了一个TArray<IQueuedWork*> QueuedWork(需要被执行的工作)…

UE4异步编程专题 - 线程池FQueuedThreadPool

1. FQueuedThreadPool & IQueuedWork FQueuedThreadPool是UE4中抽象出的线程池。线程池由若干个Worker线程&#xff0c;和一个同步队列构成。UE4把同步队列执行的任务抽象为IQueuedWork. 线程池的同步队列&#xff0c;就是一个IQueuedWork的队列了。借用wiki上线程池的图,…

UE4异步编程专题 - 多线程

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

坑爹的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;时长…

Python用subprocess的Popen来调用系统命令

当我们须要调用系统的命令的时候&#xff0c;最先考虑的os模块。用os.system()和os.popen()来进行操作。可是这两个命令过于简单&#xff0c;不能完毕一些复杂的操作&#xff0c;如给执行的命令提供输入或者读取命令的输出&#xff0c;推断该命令的执行状态&#xff0c;管理多个…

7700装win7

1.可能不能安装版本太新的win7系统,会蓝屏 2.第一次重启后,系统会提示硬件太新,系统不支持,不用理会.可以用shiftF10,进入windows/system32/oobe目录,执行msoobe手动安装. 3.第一次进入系统后,尽早关闭系统更新,除了在控制面板中关闭,还要在services.msc中关闭windows update服…

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…

Noip 2014酱油记+简要题解

好吧&#xff0c;day2T1把d默认为1也是醉了&#xff0c;现在只能期待数据弱然后怒卡一等线吧QAQDay0 第一次下午出发啊真是不错&#xff0c;才2小时左右就到了233&#xff0c;在车上把sao和fate补掉就到了 然后到宾馆之后&#xff0c;没wifi的生活就是惨啊QAQ 把空境补完就睡了…

一个取消事件的简单js例子(事件冒泡与取消默认行为)

先上代码&#xff1a; <div idouter onclickalert("我是outer")><div id"middle" onclickalert("我是middle")><div id"inner" onclickmyBubble(arguments[0]);alert("我是inner")><a onclickmyDefaul…

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; 就需要把我们的异常向量表拷贝到这个地址上。或者我们可以在链接脚本中直接指定代码的地址。 如果在主…

unity3d教程游戏包含的一切文件导入资源

http://www.58player.com/blog-2327-954.html 导入资源 将文件增加至工程文件夹的资源 (Assets) 文件夹后&#xff0c;Unity 将自动检测文件。将任何资源 (Assets) 放入资源文件夹后&#xff0c;资源 (Assets) 将显现在工程视图 (Project View) 中。 此工程视图 (Project Vie…

javascript 事件知识集锦

1.事件委托极其应用 转载的链接&#xff1a; http://www.webhek.com/event-delegate/#comments 2. 解析javascript事件机制 转载链接&#xff1a; http://www.nowamagic.net/javascript/js_EventAnalysis.php转载于:https://www.cnblogs.com/alicePanZ/p/4097017.html

ubuntu软件(查看文件差异)

你可以在ubuntu系统自带的软件---》ubuntu软件中心输入&#xff1a;meld diff 就可以安装。转载于:https://www.cnblogs.com/kobigood/p/4097411.html