UE4的多线程和tick

简介

多线程在任何语言中都是不可或缺的,意义重大的,因此,能熟练使用多线程往往是一件能让工作事半功倍的事情。那么做为强大的UE4引擎,又给了我们什么支持呢?很多人可能知道runable,知道tick但是这都只是UE4多线程的一部分,我开始也就知道这些,然后用起来总觉得哪里不对劲,经过一段时间,终于找到了适合自己的方法,不得不感慨下,UE4的东西真的是太多了,还是需要多多研究,然后再这里分享下我知道的一些好用的方法。

常用方法

Tick

这个应该是UE4最常见的方法了,我们都知道在UE4里面,最基础的类是Uobject,而继承自Uobject的AActor,,就有这个tick方法了,而地图中的大部分对象,都是继承自AActor的,除了AActor,component和UMG也都有对应的tick机制。而这也就意味着,大多数的时候,我们只需要重载基类的tick就可以使用tick了。

TimerManager

有时候我们可能需要好几个tick,或者我们不想继承自aacotor以及一些自带tick的类,这个时候我们就需要使用timer了,timer使用起来怎么说呢,不是那么好用吧,因为UE4是以世界为驱动的,所以我们的timer并不能离开世界,这点是我不太能接受的,但是不管怎么说,timer还是有的,只是不像其语言用起来那么方便,而且即使不方便也就是一点点。

  1. 我们在头文件中声明timer句柄
FTimerHandle m_hTimerHandle;
  1. 在初始化的时候使用timermanager设置好timer
    SetTimer方法有很多个版本,以下是其中比较常见的一种用法,下面注释了每个变量的含义,需要补充的是,这个函数还可以再添加一个变量,如果再加一个变量,这个变量表达的含义是第一次事件需不需要延迟触发,如果设置为小于0就是不延迟,设置为大于等于0的值就会延迟设定事件后触发,其实这里这样表述是有点问题的,实际上这个设置并不是延迟,更准确的说法是设置,就是本来是5秒触发一次的定时器,如果你设置这个延迟值为3秒,并不是8秒后触发,而是3秒。
    另外一个就是这里你发现我们使用了GetWord()去设置这个timer,也就是说,你必须能使用这个方法,而怎么使用这个方法呢,有些uobject是可以直接取到这个函数的,但是调用不了的时候怎么办呢,一般的做法是,将instance的指针传过来,通过instance去启动这个定时器。这样设计代码看起来就怪怪的,这也是我不太喜欢的地方。
GetWord()->GetTimerManager().SetTimer(
  m_hTimerHandle,//timer的句柄
  this,//处理事件的对象
  &UNetPlayManager::TimerTick,//处理事件的方法
  1.0,//触发间隔时间
  true//循环触发
);
  1. 如果你不想用这个定时器了,可以用ClearTimer清除掉
 GetWorld()->GetTimerManager().ClearTimer(m_hTimerHandle);
  1. 另外timer还支持pause和unpause,支持修改回调函数SetTimerForNextTick,这些就不细说了,也不怎么常用,真需要去源码里面看一下就知道怎么用了。

AsyncTask

这个方法主要用来把一些任务转交给主线程处理。当然你也可以选择交给任何其他有名或者无名的线程。

AsyncTask(ENamedThreads::GameThread, [=]()
  {
    //….一些操作
  });

使用起来就这么简单,这里需要说明的是,因为这个任务是丢给别的线程处理,实际上不是用来处理多线程的事件的,至少,它不具备处理循环的能力,你这里如果丢一个无限循环进去,那不光是你自己的目的达不到,还会卡住了其他线程。所以这里实际算是一个异步的调用,这句话的实际意思就是,我这里有个任务,我自己不做,给别人做了,但是这个事只能是一次性的,而且,你给人家做,人家还指不定什么时候做,做完了也不会通知你,所以这里相当于一个异步的调用。

Async

Async和 AsyncTask有比较少的区别,主要在第一个参数上面:
TaskGraph是将其放到任务图中去执行
Thread则是在单独的线程中执行
TreadPool则是放入线程池中去执行。
而AsyncTask的第一个参数则是将任务放在指定的线程执行。

FAutoDeleteAsyncTask

FAutoDeleteAsyncTask是一个模板,使用友元的特性,让模板类可以操作类的私有函数,算是学到了,不错的用法,有兴趣的可以自己研究下。要使用FAutoDeleteAsyncTask,需要实现几个函数:

  1. DoWork() 任务执行的具体语句
  2. GetStatId 任务id
    除此之外还需要继承FNonAbandonableTask,这是因为在任务管理中会有执行丢弃线程的相关操作,我们首先要保证任务管理者在调用相关函数的时候不出现异常,其次我们也不希望我们的任务被丢弃,如果希望被丢弃,可以不继承这个FNonAbandonableTask,相应的实现FNonAbandonableTask,让这个任务能被抛弃,也就是在执行抛弃的时候清除自己,并且把变量设置为可以清楚。
class ExampleAsyncTask : public FNonAbandonableTask
  {
  	friend class FAutoDeleteAsyncTask<ExampleAsyncTask>;

  	int32 ExampleData;

  	ExampleAsyncTask(int32 InExampleData)
  	 : ExampleData(InExampleData)
  	{
  	}

  	void DoWork()
  	{
  		... do the work here
  	}

  	FORCEINLINE TStatId GetStatId() const
  	{
  		RETURN_QUICK_DECLARE_CYCLE_STAT(ExampleAsyncTask, STATGROUP_ThreadPoolAsyncTasks);
  	}
  };

定义好之后有两种方式启动线程,区别主要是任务在哪个线程执行。如果是丢到线程池的话,可以自己指定线程池,如果不指定,就在一个全局的线程池QueuedPool中

// 将任务扔到线程池中去执行
(new FAutoDeleteAsyncTask<ExampleAsyncTask>(5))->StartBackgroundTask();
// 直接在当前线程执行操作
(new FAutoDeleteAsyncTask<ExampleAsyncTask>(5))->StartSynchronousTask();

FAutoDeleteAsyncTask的优点在于任务执行完之后,任务会被自动删除掉,不需要任何其他操作。下面是FAutoDeleteAsyncTask的dowork函数,可以看到,task的任务执行完成之后,函数就会自己清除自己。

void DoWork()
	{
		LLM_SCOPE(InheritedLLMTag);
		FScopeCycleCounter Scope(Task.GetStatId(), true);

		Task.DoWork();
		delete this;
	}

FAsyncTask

和上面的FAutoDeleteAsyncTask实现一样,区别在于,不进行后台运行任务的时候,需要自己进行一些处理,保证任务完成之后自己删除任务。

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;

FRunnable

这个类也是我比较喜欢的开启多线程的类,直接继承FRunnable,然后重载三个函数:
Init() 初始化线程的一些参数
Run() 任务主体
Exit() 线程结束清理
执行顺序是:先执行Init,如果失败就不会执行Run(),Run()执行完成就会执行Exit()。
然后在使用的时候不能直接使用,需要使用FRunnableThread::create来启动程序。
这里有两种用法:

  1. 在构造函数中调用。
FRunnableThread::create(this,TEXT("name");
  1. 在外部new了之后再调用
FRunnable* runnable = new FRunnableTest();
FRunnableThread::create(runnable,TEXT("name");

不管哪一种用法,最好都要把create返回的句柄保存一下,在线程对象清除的时候也要清除这个句柄。

一些新发现

FTickableGameObject

这个就是开篇提到的,依赖最小的线程。一直以来我都喜欢职责单一单一原则,和解耦合,所以喜欢用些尽量独立的方法,让方法模块自己本身也更独立。如果同时又能很方便使用,那就最好了。
说起FTickableGameObject最大的好处,应该就是不需要是原生的UE4的类就可以使用了,本来它自己也是F开头的类,也就是普通类。想要使用它,只需要直接继承这个类然后实现几个函数就可以了。

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(threadname, STATGROUP_Tickables);
}

因为Tick 和GetStatId是纯虚函数,所以这里必须要实现,另外两个可以看到只是做下设置。
重载的时候tick就不需要多说了,GetStatId里面只要修改一个独一无二的字符串做id就可以了。threadname不需要用双引号写成字符串的形式,这里是一个宏的参数,你写成什么样子,最后就叫啥。

线程的同步

FCriticalSection

这个需要和FScopeLock 配合使用。

  1. 在头文件中定义一个FCriticalSection对象
FCriticalSection CriticalSection;
  1. 在需要加锁的地方声明如下,这里声明的是局部的变量,这个lock出了代码块就会自动释放,不需要收到释放,可以说是很方便了。
FScopeLock Lock(&CriticalSection);

暂时写这么多吧,我也是刚开始研究,有什么不对的地方,欢迎指出,大家一起学习,一起进步。

猜你喜欢

转载自blog.csdn.net/u012505629/article/details/109640790
今日推荐