Loading

UE5-MultiThread

Thread

FRunnable与FRunnableThread

FRunnable定义的是“接口函数”,FRunableThread是“线程实体”。在FRunnableThread的执行周期中会调用到FRunnable中的虚函数,因此通常情况下我们都会派生FRunnable

FRunnableThread可以类比标准库中的std::thread。它是FRunnableThread的子类,在Windows平台上使用,FRunnableThread有一个类成员:

/** The thread handle for the thread. */
HANDLE Thread = 0;

FRunnableThread中有一个类成员为

/** The runnable object to execute on this thread. */
FRunnable* Runnable;

线程的创建以及任务的的分配为

class FTestRunnable : public FRunnable {
public:
	FTestRunnable();
	virtual ~FTestRunnable() override;
protected:
	TObjectPtr<FRunnableThread> RunnableThread;
};
// 传入自身供作为线程生命周期执行任务调用
FTestRunnable::FTestRunnable()
    : RunnableThread(FRunnableThread::Create(this, TEXT("FTestRunnableThread"))) {}

FTestRunnable::~FTestRunnable() { delete RunnableThread; }

FRunnableThread::Create

这一步中包含了许多操作,例如

  • 向操作系统申请线程,同时绑定申请成功的回调
  • 回调中会将线程注册到全局FThreadManager中,然后调用FRunnable中定义的各种虚方法

实际创建线程的代码如下所示,_ThreadProc作为创建成功的回调函数

// virtual FRunnableThreadWin::CreateInternal
Thread = CreateThread(NULL, InStackSize, _ThreadProc, this, 	
                      STACK_SIZE_PARAM_IS_A_RESERVATION | CREATE_SUSPENDED, (::DWORD*)&ThreadID);
/**
 * The thread entry point. Simply forwards the call on to the right
 * thread main function
 */
static ::DWORD STDCALL _ThreadProc(LPVOID pThis)
{
    check(pThis);
    auto* ThisThread = (FRunnableThreadWin*)pThis;
    // 添加到线程管理类中
    FThreadManager::Get().AddThread(ThisThread->GetThreadID(), ThisThread);
    return ThisThread->GuardedRun();
}

_ThreadProc中会依次执行FRunnable中的接口函数

uint32 FRunnableThreadWin::Run() {
    uint32 ExitCode = 1;
    if (Runnable->Init() == true) {
        // 执行对应的工作
        ExitCode = Runnable->Run();
        Runnable->Exit();
        // ...
    }
    return ExitCode;
}

Fake Thread

当系统不支持多线程时,UE将会在主线程上分时间片去Tick一个"Fake Thread"

FRunnableThread* FRunnableThread::Create(
    class FRunnable* InRunnable, 
    const TCHAR* ThreadName,
    uint32 InStackSize,
    EThreadPriority InThreadPri, 
    uint64 InThreadAffinityMask,
    EThreadCreateFlags InCreateFlags)
{
    bool bCreateRealThread = FPlatformProcess::SupportsMultithreading();
    FRunnableThread* NewThread = nullptr;
    if (bCreateRealThread) {
        // ...
    }
    else if (InRunnable->GetSingleThreadInterface()) {
        // Create a fake thread when multithreading is disabled.
        NewThread = new FFakeThread();
    }

    // Init... Request to OS to create new thread
    return NewThread;
}

FFakeThreadFRunableThreadWin都继承自FRunableThread,与FRunableThreadWin不同的时,在调用FRunnableThread::Create时,FFakeThread不会向操作系统申请线程资源,而是将自己注册到FThreadManager中,由FThreadManager来调用自身的Tick

class FFakeThread : public FRunnableThread {
    /** Runnable object associated with this thread. */
    FSingleThreadRunnable* SingleThreadRunnable;

    /** Tick one time per frame. */
    virtual void Tick() override
    {
        if (SingleThreadRunnable && !bIsSuspended)
        {
            SingleThreadRunnable->Tick();
        }
    }
};

为了确保程序能正常在支持多线程和只支持单线程的机器上运行,通常会同时继承FRunnable以及FSingleThreadRunnable

可以视情况只继承FRunnable;但若要使用FSingleThreadRunnable,则必须和FRunnable同时使用

class FUdpSocketSender : public FRunnable, private FSingleThreadRunnable 
{
    /**
	 * Gets single thread interface pointer used for ticking this runnable when multi-threading is disabled.
	 * If the interface is not implemented, this runnable will not be ticked when FPlatformProcess::SupportsMultithreading() is false.
	 *
	 * @return Pointer to the single thread interface or nullptr if not implemented.
	 */
    virtual FSingleThreadRunnable* GetSingleThreadInterface() override { return this; }

    virtual uint32 Run() override 
    {
        while (!Stopping) 
        {
            // Loop...
        }
        return 0;
    }

    virtual void Tick() override
    {
        // Execute Per Tick
    }
};

FThreadManager

全局单例

class CORE_API FThreadManager {
    static FThreadManager& Get() {
        static FThreadManager Singleton;
        return Singleton;
    }
};

FThreadManager的Tick将在Gameplay主线程Tick之后调用,且只会执行FakeThread或ForkableThread

/** List of thread objects to be ticked. */
TMap<uint32, class FRunnableThread*, TInlineSetAllocator<256>> Threads;

void FThreadManager::Tick()
{	
    const bool bIsSingleThreadEnvironment = FPlatformProcess::SupportsMultithreading() == false;
    if (bIsSingleThreadEnvironment)
    {
        FScopeLock ThreadsLock(&ThreadsCritical);
        // Tick all registered fake threads.
        for (TPair<uint32, FRunnableThread*>& ThreadPair : Threads)
        {
            // Only fake and forkable threads are ticked by the ThreadManager
            if( ThreadPair.Value->GetThreadType() != FRunnableThread::ThreadType::Real )
            {
                ThreadPair.Value->Tick();
            }
        }
    }
}
void FPreLoadScreenManager::GameLogicFrameTick() {
    //We have to manually tick everything as we are looping the main thread here
    FTaskGraphInterface::Get().ProcessThreadUntilIdle(ENamedThreads::GameThread);
    FTSTicker::GetCoreTicker().Tick(DeltaTime);
    FThreadManager::Get().Tick();
    // ...
}

Terminate FRunnableThread

FRunnableThread的执行逻辑中,会依次执行FRunnable::InitFRunnable::RunFRunnable::Exit。对于FRunnable::Stop一般情况下会在FRunnableThread的析构函数中调用

FRunnable::Stop的调用和线程被销毁没有必然联系,可以通过主动调用来执行任务的收尾操作,然后更改标志位让FRunnable::Run中的while循环主动退出(此操作会导致线程对象被操作系统回收吗,是否涉及到TaskGraph

~FRunnableThreadWin()
{
    if (Thread)
    {
        Kill(true);
    }
}

/**
 * Tells the thread to exit. If the caller needs to know when the thread has exited, it should use the bShouldWait value.
 * It's highly recommended not to kill the thread without waiting for it.
 * Having a thread forcibly destroyed can cause leaks and deadlocks.
 * 
 * The kill method is calling Stop() on the runnable to kill the thread gracefully.
 * 
 * @param bShouldWait	If true, the call will wait infinitely for the thread to exit.					
 * @return Always true
 */
virtual bool Kill(bool bShouldWait = false) override
{
    check(Thread && "Did you forget to call Create()?");
    bool bDidExitOK = true;

    // Let the runnable have a chance to stop without brute force killing
    if (Runnable)
    {
        Runnable->Stop();
    }

    if (bShouldWait == true)
    {
        // Wait indefinitely for the thread to finish.  IMPORTANT:  It's not safe to just go and
        // kill the thread with TerminateThread() as it could have a mutex lock that's shared
        // with a thread that's continuing to run, which would cause that other thread to
        // dead-lock.  
        //
        // This can manifest itself in code as simple as the synchronization
        // object that is used by our logging output classes

        // 通常情况是在Gameplay Thread中析构FRunnable对象 进而析构FRunnableThread对象
        // 最后Gameplay Thread线程阻塞等待
        WaitForSingleObject(Thread, INFINITE);
    }

    CloseHandle(Thread);
    Thread = NULL;
	// ...
    return bDidExitOK;
}

Synchronization

FCriticalSection/FScopeLock

std::mutex/std::lock_guard

/**
 * This is the Windows version of a critical section. It uses an aggregate
 * CRITICAL_SECTION to implement its locking.
 */

FRWLock/FRWScopeLock

class ThreadSafeArray
{
public:
    int32 Get(int32 Index)
    {
        FRWScopeLock ScopeLock(ValuesLock, SLT_ReadOnly);
        return Values[Index];
    }

    void Add(int32 Value)
    {
        FRWScopeLock ScopeLock(ValuesLock, SLT_Write);
        Values.Add(Value);
    }

private:
    FRWLock ValuesLock;
    TArray<int32> Values;
};
/**
 * FWindowsRWLock - Read/Write Mutex
 * - Provides non-recursive Read/Write (or shared-exclusive) access.
 * - Windows specific lock structures/calls Ref: https://msdn.microsoft.com/en-us/library/windows/desktop/aa904937(v=vs.85).aspx
 */

FEvent

std::condition_variable

// FGenericPlatformProcess
UE_DEPRECATED(5.0, "Please use GetSynchEventFromPool to create a new event, and ReturnSynchEventToPool to release the event.")
static class FEvent* CreateSynchEvent(bool bIsManualReset = false);

FEvent作为操作系统资源,一般从池子中获取,再归还到池子中

FEvent* FGenericPlatformProcess::GetSynchEventFromPool(bool bIsManualReset)
{
   return bIsManualReset
      ? TLazySingleton<TEventPool<EEventMode::ManualReset>>::Get().GetEventFromPool()
      : TLazySingleton<TEventPool<EEventMode::AutoReset>>::Get().GetEventFromPool();
}

void FGenericPlatformProcess::ReturnSynchEventToPool(FEvent* Event)
{
	if( !Event )
	{
		return;
	}

	if (Event->IsManualReset())
	{
		TLazySingleton<TEventPool<EEventMode::ManualReset>>::Get().ReturnToPool(Event);
	}
	else
	{
		TLazySingleton<TEventPool<EEventMode::AutoReset>>::Get().ReturnToPool(Event);
	}
}
// true means the event requires manual reseting
// reset means resets the event to an untriggered (waitable) state
FEvent* SyncEvent = FPlatformProcess::GetSynchEventFromPool(true);

// somewhere wait
SyncEvent->Wait();
// somewhere trigger
SyncEvent->Trigger();

FPlatformProcess::ReturnSynchEventToPool(SyncEvent);

由于FEvent通常需要手动创建和手动释放,那么也因此有了RAII的做法

// 类似unique的FEvent 但non-copyable, non-movable
class CORE_API FEventRef final
{
public:
    // 在构造和析构函数中申请和释放
    explicit FEventRef(EEventMode Mode = EEventMode::AutoReset);
    ~FEventRef();
    
	// Delete special member functions...
private:
    FEvent* Event;
};
class CORE_API FSharedEventRef final
{
public:
	explicit FSharedEventRef(EEventMode Mode = EEventMode::AutoReset) :
    	Ptr(TLazySingleton<TEventPool<EEventMode::AutoReset>>::Get().GetRawEvent(),
      [](FEvent* Event) { 			 
          TLazySingleton<TEventPool<EEventMode::AutoReset>>::Get().ReturnRawEvent(Event); 
      }) {}
    
	FEvent* operator->() const { return Ptr.Get(); }
private:
	TSharedPtr<FEvent> Ptr;
};

TAtomic

std::atomic

LockFree Programing

UE并发-无锁编程及其在TaskGraph中的应用

UE并发-生产者消费者模式

AsyncTask

本质上还是使用FRunnableThread以及FRunnable,只是进行了一定程度的封装

Init ThreadPool

在初始化线程池的时候其实还分了是否处在编辑器中,以下代码是WITH_EDITOR 0的情况

// FEngineLoop::PreInitPreStartupScreen
// 全局线程池指针
GThreadPool = FQueuedThreadPool::Allocate();
int32 NumThreadsInThreadPool = FPlatformMisc::NumberOfWorkerThreadsToSpawn();

// we are only going to give dedicated servers one pool thread
if (FPlatformProperties::IsServerOnly()) {
    NumThreadsInThreadPool = 1;
}
verify(GThreadPool->Create(NumThreadsInThreadPool, StackSize, TPri_SlightlyBelowNormal, TEXT("ThreadPool")));

GThreadPool->Create中会根据线程池中创建的线程的数量,创建对应的FQueuedThread以及FRunnableThread

由于FQueuedThread携带了条件变量FEvent,因此在线程被创建准备执行任务时,将会被阻塞,直至有任务被分配

uint32 FQueuedThread::Run()
{
    while (!TimeToDie.Load(EMemoryOrder::Relaxed))
    {
        // We need to wait for shorter amount of time
        bool bContinueWaiting = true;
        if (bContinueWaiting)
        {
            DoWorkEvent->Wait();
        }

        IQueuedWork* LocalQueuedWork = QueuedWork;
        QueuedWork = nullptr;
        FPlatformMisc::MemoryBarrier();
        // well you woke me up, where is the job or termination request?
        check(LocalQueuedWork || TimeToDie.Load(EMemoryOrder::Relaxed)); 
        while (LocalQueuedWork)
        {
            // Tell the object to do the work
            LocalQueuedWork->DoThreadedWork();
            // Let the object cleanup before we remove our ref to it
            LocalQueuedWork = OwningThreadPool->ReturnToPoolOrGetNextJob(this);
        }
    }
    return 0;
}

Add Task

// FAutoDeleteAsyncTask - template task for jobs that delete themselves when complete
// Sample code:
class ExampleAutoDeleteAsyncTask : public FNonAbandonableTask
{
    friend class FAutoDeleteAsyncTask<ExampleAutoDeleteAsyncTask>;

    int32 ExampleData;
    ExampleAutoDeleteAsyncTask(int32 InExampleData) : ExampleData(InExampleData) {}

    void DoWork() { /* ... do the work here */ }
    
    FORCEINLINE TStatId GetStatId() const
    {
        RETURN_QUICK_DECLARE_CYCLE_STAT(ExampleAutoDeleteAsyncTask, STATGROUP_ThreadPoolAsyncTasks);
    }
};


void Example()
{
    // start an example job
    new FAutoDeleteAsyncTask<ExampleAutoDeleteAsyncTask>(5)->StartBackgroundTask();
    // do an example job now, on this thread
    new FAutoDeleteAsyncTask<ExampleAutoDeleteAsyncTask>(5)->StartSynchronousTask();
}
// FAsyncTask - template task for jobs queued to thread pools
// Sample code:
class ExampleAsyncTask : public FNonAbandonableTask
{
    friend class FAsyncTask<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);
    }
};

void Example()
{
    // start an example job

    FAsyncTask<ExampleAsyncTask>* MyTask = new FAsyncTask<ExampleAsyncTask>(5);
    
    MyTask->StartBackgroundTask();
    // --or --
    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;
}
  • FAutoDeleteAsyncTask在执行任务流程后通过delete this自动完成销毁操作;FAsyncTask则需要手动销毁
  • StartBackgroundTask会调用QueuedPool->AddQueuedWork来将任务注册到优先级任务队列中,以此来分配线程工作
  • 线程池中的FQueuedThread对象在完成当前分配的任务后,会调用OwningThreadPool->ReturnToPoolOrGetNextJob来将任务从任务队列中移除,然后寻找是否有排队的任务需要完成

如果要自定义Task,需要完成以下几件事情

  • 确保类中具有void DoWork()TStatId GetStatId()bool CanAbandon()void Abandon()四个成员函数。FAsyncTask<Task>FAutoDeleteAsyncTask<Task>作为模板类,已经将Task作为类成员并这四个函数进行了调用,如果不定义将会导致编译出错

  • FAsyncTaskFAutoDeleteAsyncTask设为友元类

  • 可以继承FNonAbandonableTask,它为你预先定义好了两个函数,同时代表该Task是不可被抛弃的

    /**
     * Stub class to use a base class for tasks that cannot be abandoned
     */
    class FNonAbandonableTask
    {
    public:
        bool CanAbandon() { return false; }
        void Abandon() {}
    };
    

TaskGraph

UE并发-TaskGraph的实现和用法

《Exploring in UE4》多线程机制详解[原理分析]

UE4/UE5的TaskGraph

ENamedThreads

TaskGraph支持两种类型的线程,一种为AnyThread,一种为NamedThreadAnyThread是TaskGraph创建出来的后台线程(BackgroundThread),会持续的从优先级任务队列中那任务出来执行,类似于线程池;NamedThread例如GameThreadRHIThread需要在初始化的时候Attach到TaskGraph系统中。

namespace ENamedThreads
{
    enum Type : int32
    {
        UnusedAnchor = -1,
        StatsThread,
        RHIThread, /* 对应FRHIThread 渲染硬件接口层线程 */
        AudioThread,
        GameThread, /* 程序主线程 */
        ActualRenderingThread = GameThread + 1, /* FRenderingThread */

            /** not actually a thread index. Means "Unknown Thread" or "Any Unnamed Thread" **/
        AnyThread = 0xff, 
    }
}

在UE5中,AudioThreadStatsThread已经被从TaskGraph中移除

// StatsThread has been removed. Stats system should be used by Stats public API
// AudioThread has been removed. Please use FAudioThread API

ESubsequentsMode

namespace ESubsequentsMode
{
	enum Type
	{
		/** Necessary when another task will depend on this task. */
		TrackSubsequents,
		/** Can be used to save task graph overhead when firing off a task that will not be a dependency of other tasks. */
		FireAndForget
	};
}

CreateTask

自定义Task任务与上文中创建线程池任务颇为类似,同样需要手动加成成员函数依赖

class FMyTask
{
public:
	// 使用自带的STATGROUP_TaskGraphTasks分组
	FORCEINLINE TStatId GetStatId() const {
        RETURN_QUICK_DECLARE_CYCLE_STAT(FMyTask, STATGROUP_TaskGraphTasks) 
    }
	
	static ENamedThreads::Type GetDesiredThread() { return ENamedThreads::AnyThread; }

	// 代表该任务不存在后续任务
	static ESubsequentsMode::Type GetSubsequentsMode() { return ESubsequentsMode::FireAndForget; }
	
	void DoTask(ENamedThreads::Type CurrentThread, const FGraphEventRef& CompletionGraphEvent)
	{
		const FString CurrentThreadName = 
            FThreadManager::GetThreadName(FPlatformTLS::GetCurrentThreadId());
		// May Log Like This: CurrentThread: Foreground Worker #1
		UE_LOG(LogTemp, Log, TEXT("CurrentThread: %s"), *CurrentThreadName);
	}
};

调用接口函数创建出使用TRefCountPtr智能指针包裹住的FGraphEvent对象

// Somewhere Like Beginplay
FGraphEventRef NewTask = TGraphTask<FMyTask>::CreateTask().ConstructAndDispatchWhenReady();
/** Passthrough internal task constructor and dispatch. Note! Generally speaking references will not pass through; use pointers */
template<typename...T>
FGraphEventRef ConstructAndDispatchWhenReady(T&&... Args)
{
    new ((void *)&Owner->TaskStorage) TTask(Forward<T>(Args)...);
    // Launch Immediately
    return Owner->Setup(Prerequisites, CurrentThreadIfKnown);
}

/** Passthrough internal task constructor and hold. */
template<typename...T>
TGraphTask* ConstructAndHold(T&&... Args)
{
    new ((void *)&Owner->TaskStorage) TTask(Forward<T>(Args)...);
    // 需要对返回值进行UnLock操作才能开始执行任务
    return Owner->Hold(Prerequisites, CurrentThreadIfKnown);
}

当然也可以使用官方封装过的全局函数AsyncTask,只需要传入函数对象

void AsyncTask(ENamedThreads::Type Thread, TUniqueFunction<void()> Function)
{
	TGraphTask<FAsyncGraphTask>::CreateTask().ConstructAndDispatchWhenReady
        (Thread, MoveTemp(Function));
}

Startup TaskGraph

在引擎的初始化流程中,会初始化全局单例FTaskGraphInterface

UE中单例的实现方式很多,有使用TLazySingleton<T>的,有使用传统Meyers' Singleton的,也有使用单文件static指针的。这里TaskGraph使用的是单文件static指针

if (bCreateTaskGraphAndThreadPools)
{
    // initialize task graph sub-system with potential multiple threads
    SCOPED_BOOT_TIMING("FTaskGraphInterface::Startup");
    // 将单例指针new出来
    FTaskGraphInterface::Startup(FPlatformMisc::NumberOfWorkerThreadsToSpawn());
    FTaskGraphInterface::Get().AttachToThread(ENamedThreads::GameThread);
}

UE调用FPlatformMisc::NumberOfWorkerThreadsToSpawn()为线程池和TaskGraph计算需要创建的线程数量,在非DS的情况下,计算方式可以简化为

int32 FWindowsPlatformMisc::NumberOfWorkerThreadsToSpawn()
{	
    int32 NumberOfCores = FWindowsPlatformMisc::NumberOfCores();
    int32 NumberOfCoresIncludingHyperthreads = FWindowsPlatformMisc::NumberOfCoresIncludingHyperthreads();
    int32 NumberOfThreads = NumberOfCoresIncludingHyperthreads > NumberOfCores ?
        NumberOfCoresIncludingHyperthreads - 2 : NumberOfCores - 1;

    // need to spawn at least one worker thread (see FTaskGraphImplementation)
    return FMath::Max(NumberOfThreads, 2);
}

UE5添加了一种轻量化的TaskGraph,为FTaskGraphCompatibilityImplementation。UE5默认创建FTaskGraphCompatibilityImplementation

void FTaskGraphInterface::Startup(int32 NumThreads)
{
	if (FParse::Param(FCommandLine::Get(), TEXT("TaskGraphForceOldBackend")))
	{
		GUseNewTaskBackend = 0;
	}
	else if (FParse::Param(FCommandLine::Get(), TEXT("TaskGraphForceNewBackend")))
	{
		GUseNewTaskBackend = 1;
	}

	if (GUseNewTaskBackend)
	{
		//We want to reduce the number of overall threads that UE uses so that there is are some 
		//free cores available for other things like the Browser or other Applications. 
		//Therefore we increase the number of Foreground workers, which are mostly unused. 
		//But when HighPrio work comes in the Foreground workers will be available and get the job done.
		// ...
		TaskGraphImplementationSingleton = new FTaskGraphCompatibilityImplementation(NumThreads);
	}
	else
	{
		// TaskGraphImplementationSingleton is actually set in the constructor because find work will be called before this returns.
		new FTaskGraphImplementation(NumThreads); 
	}
}

如果创建的是FTaskGraphImplementation,那么在TaskGraph创建的过程中就会将所有的NamedThread和AnyThread都创建出来,并赋值进WorkerThreads数组中

如果创建的是FTaskGraphCompatibilityImplementation,那么在TaskGraph创建的时候,它首先会将AnyThread的创建交给FScheduler去处理;且它并不负责创建实际的NamedThread实体,而是会留在引擎的后续初始化流程中,由各个对应的模块创建。也就是说TaskGraph本身并不持有指向各种线程的指针(真的是这样吗?Gameplay ActorTick的是在哪个部分添加的?物理线程的循环Tick是在哪里添加的?对于FTaskGraphCompatibilityImplementation而言任务是如何分配到AnyThread的

AActor::Tick

Delegate And Function Task

Other Task

UE Stats

UE的一种基于埋点的性能统计机制,通过Stat StartFileStat StopFile两个控制台指令来开始和结束录制性能分析文件

性能分析文件的保存目录为,Saved\Profiling\UnrealStats\,格式为uestats

在UE5中通过Tools\Session Frontend打开分析窗口

在控制台中启动监视的时候通常以组为单位,即使用Stat GroupDesc

SCOPE_CYCLE_COUNTER

通过函数作用域实现开始和停止性能统计,一个基础的COUNTER有三部曲

// GroupDescription GroupID GroupCategory
DECLARE_STATS_GROUP(GroupDesc, GroupId, GroupCat)

DECLARE_CYCLE_STAT(CounterName,StatId,GroupId)
   
// StatID
SCOPE_CYCLE_COUNTER(Stat)

通常使用中需要定义一个组,定义一个埋点变量名,最后根据变量名创建埋点对象

// xx.h
// 通常使用STATCAT_Advanced
DECLARE_STATS_GROUP(TEXT("SharedMemory"), STATGROUP_SharedMemory, STATCAT_Advanced)
// xx.cpp
DECLARE_CYCLE_STAT(TEXT("SharedMemory_Tick"), STAT_SharedMemory_Tick, STATGROUP_SharedMemory)
// 可以为组声明更多的埋点...
    
void ASharedMemoryActor::Tick(float DeltaTime) 
{
    // 在当前作用域创建FScopeCycleCounter对象
	SCOPE_CYCLE_COUNTER(STAT_SharedMemory_Tick)
    // Do Something...
}

组宏的命名方式通常为STATGROUP_XXX,埋点变量名的命名方式通常为STAT_XXX。但这并不是固定的

DECLARE_SCOPE_CYCLE_COUNTER

这个宏相当于上一个条目的一个整合,他将结构体的声明以及静态对象的创建安排在了作用域中,本质上是一样的

#define DECLARE_SCOPE_CYCLE_COUNTER(CounterName,Stat,GroupId) \
	DECLARE_STAT(CounterName,Stat,GroupId,EStatDataType::ST_int64, EStatFlags::ClearEveryFrame | EStatFlags::CycleStat, FPlatformMemory::MCR_Invalid); \
	static DEFINE_STAT(Stat) \
	FScopeCycleCounter CycleCount_##Stat(GET_STATID(Stat), GET_STATFLAGS(Stat));
// 同样首先需要有一个组的定义
DECLARE_STATS_GROUP(GroupDesc, GroupId, GroupCat)

DECLARE_SCOPE_CYCLE_COUNTER(CounterName,Stat,GroupId)

使用的时候StatID会动态创建,即在首次调用Tick的时候创建

// xx.h
// 通常使用STATCAT_Advanced
DECLARE_STATS_GROUP(TEXT("SharedMemory"), STATGROUP_SharedMemory, STATCAT_Advanced)
void ASharedMemoryActor::Tick(float DeltaTime) 
{
    // 在当前作用域创建FScopeCycleCounter对象
	DECLARE_SCOPE_CYCLE_COUNTER(TEXT("SharedMemory_Tick"), STAT_SharedMemory_Tick, STATGROUP_SharedMemory)
    // Do Something...
}

QUICK_SCOPE_CYCLE_COUNTER

很多时候都会把GroupID与GroupDescription,StatID与CounterName起成相同的名字,因此这个宏就是一层更简便的封装,它归属于STATGROUP_Quick

#define QUICK_SCOPE_CYCLE_COUNTER(Stat) \
	DECLARE_SCOPE_CYCLE_COUNTER(TEXT(#Stat),Stat,STATGROUP_Quick)

虚幻4 QUICK_SCOPE_CYCLE_COUNTER宏拆解

衍生宏,常用于返回TStatId时使用

#define RETURN_QUICK_DECLARE_CYCLE_STAT(StatId,GroupId) \
	DECLARE_STAT(TEXT(#StatId),StatId,GroupId,EStatDataType::ST_int64, EStatFlags::ClearEveryFrame | EStatFlags::CycleStat, FPlatformMemory::MCR_Invalid); \
	static DEFINE_STAT(StatId) \
	return GET_STATID(StatId);

FScopeCycleCounterUObject

专门为UObject使用的埋点

posted @ 2022-07-26 17:10  _FeiFei  阅读(2073)  评论(1编辑  收藏  举报