Multithreading
Overview
In the Unreal Engine, the main thread is known as the Game Thread.
There are multiple ways to do multithreading work in the Unreal Engine.
- Async Tasks
- Runnable Threads
- Queued Thread Pools
Platforms
To determine if the target supports multithreading, call the FPlatformProcess::SupportsMultithreading
function.
The HTML5 platform is single-thread when compiled without support for Emscripten pthreads (POSIX Threads).
Other platforms can be compiled with a single-threaded model (DEFAULT_NO_THREADING
symbol).
Additionally, single-threaded can be enforced with the "-nothreading
" mode set in the command-line.
Queued Thread Pools
Queued thread pools are represented by the FQueuedThreadPool
interface.
It used as a callback by FQueuedThreads
to queue asynchronous work for callers using background threads.
There is a global thread pool for shared async operations called GThreadPool
.
API
To create a queue thread pool, call the Create
function.
bool Create(
uint32 InNumQueuedThreads,
uint32 StackSize,
EThreadPriority ThreadPriority
);
InNumQueuedThreads
– The number of threads to use in the pool.
StackSize
– The size of stack, in bytes used by the threads in the pool (default is 32k).
ThreadPriority
– The priority of the threads in the pool (default isTPri_Normal
).
To destroy a queue thread pool, call the Destroy
function.
The function releases all the threads in the pool.
void Destroy();
To add a thread to the pool, call the AddQueuedWork
function with a IQueuedWork
object.
void AddQueuedWork( IQueuedWork* InQueuedWork );
To remove a thread from the pool, call the RetractQueuedWork
function with a IQueuedWork
object.
bool RetractQueuedWork( IQueuedWork* InQueuedWork );
Internals
If the client supports multithreading (FPlatformProcess::SupportsMultithreading
), the GThreadPool
global thread pool is created in FEngineLoop::PreInit
.
The pool is allocated with the FQueuedThreadPool::Allocate
function.
GThreadPool = FQueuedThreadPool::Allocate();
Then created with the FQueuedThreadPool::Create
function.
- The number of threads is determined by
FPlatformMisc::NumberOfWorkerThreadsToSpawn
.
- The stack is 128k.
- The priority of the treads is
TPri_Lowest
.
int32 NumThreadsInThreadPool = FPlatformMisc::NumberOfWorkerThreadsToSpawn();
GBackgroundPriorityThreadPool->Create(NumThreadsInThreadPool, 128 * 1024, TPri_Lowest);
GThreadPool
is released in FEngineLoop::AppPreExit
.
if (GThreadPool != nullptr)
{
GThreadPool->Destroy();
}
Usage
- The global thread pool
GThreadPool
.
- The sound file loading pool
FileLoadingThreadPool
.
- The distance field atlas builder
FDistanceFieldAsyncQueue
(Editor only).
GThreadPool
is used to:
- Version control operations (Editor only).
- The encoding of the shadow map textures.
- Audio decompression.
- Texture streaming.
Runnable Threads
Runnable Objects
Runnable objects are represented by the FRunnable
interface.
A runnable object is executed on an arbitrary thread.
A runnable object is implemented by overriding the following functions:
Init
– Initializes the thread.
Run
– The threaded work is performed.
Stop
– Requests to terminate the thread early.
Exit
– Cleans up the thread.
Threads
Runnable threads are represented by the FRunnableThread
interface.
To create a runnable thread, call the Create
function.
You can specify the runnable, the stack size and thread priority.
static FRunnableThread * Create
(
class FRunnable * InRunnable,
const TCHAR * ThreadName,
uint32 InStackSize,
EThreadPriority InThreadPri,
uint64 InThreadAffinityMask
)
Example
Declare a custom runnable type.
class FMyRunnable : public FRunnable
{
public:
virtual bool Init() override;
virtual uint32 Run() override;
virtual void Stop() override;
virtual void Exit() override;
};
Implement the FRunnable
functions.
bool FMyRunnable::Init()
{
...
}
uint32 FMyRunnable::Run()
{
...
}
void FMyRunnable::Stop()
{
...
}
void FMyRunnable::Exit()
{
...
}
Declare variables to store the runnable object and runnable thread.
FMyRunnable* MyRunnable = nullptr;
FRunnableThread* MyThread = nullptr;
Create the runnable.
if (FPlatformProcess::SupportsMultithreading())
{
MyRunnable = new FMyRunnable();
MyThread = FRunnableThread::Create(FMyRunnable, TEXT("My Thread"));
}
Usages
- Thread Heartbeat, to check for hangs (
FThreadHeartBeat
).
- Audio thread (
FAudioThread
).
- TCP Listener (
FTcpListener
).
- UDP Socket Sender and Receiver (
FUdpSocketSender
,FUdpSocketReceiver
).
Async Tasks
Async tasks are the most common usage of multithreading.
There are two types of async tasks: FAsyncTask
and FAutoDeleteAsyncTask
.
The only difference is that FAutoDeleteAsyncTask
can delete itself automatically when the task is complete.
Create
To create a new async task, declare a new class that derive from FNonAbandonableTask
.
This class performs the threaded work.
Declare this class as a friend class of FAsyncTask
using the type of the class as the template argument.
Define the DoWork
function that will be called when the task is running.
Define the GetStatId
method.
Run
To start an async task:
- Call the
StartBackgroundTask
function to queue the task in the background thread pool.void StartBackgroundTask(FQueuedThreadPool * InQueuedPool);
- Or call the
StartSynchronousTask
function to run the task on the current thread.void StartSynchronousTask();
Example
Async Task
- Declare a custom task class that derive from
FNonAbandonableTask
.
- Declare the class as a friend class of
FAsyncTask
.
- Implement the
DoWork
function to perform the threaded work.
- Include the
GetStatId
inline function.class FMyTask : public FNonAbandonableTask { friend class FAsyncTask<FMyTask>; protected: void DoWork() { ... } FORCEINLINE TStatId GetStatId() const { RETURN_QUICK_DECLARE_CYCLE_STAT(FMyTask, STATGROUP_ThreadPoolAsyncTasks); } };
- Declare the task as a member of the owner type.
FAsyncTask<FMyTask> * MyTask;
- Create and run the task.
- Create an instance of
FAsyncTask
for the task.
- Start the task by calling the
StartBackgroundTask
function.
MyTask = new FAsyncTask<FMyTask>(); MyTask->StartBackgroundTask();
- Create an instance of
- When the owner type is destroyed, clean up the task.
- Try to cancel the task by calling the
Cancel
function.
- If it fails, call the
EnsureCompletion
function to wait until the task is complete.
- Delete the task.
if (MyTask && !MyTask->Cancel()) { MyTask->EnsureCompletion(); } delete MyTask; MyTask = nllptr;
- Try to cancel the task by calling the
Auto Delete Async Task
- Declare a custom task class as a friend class of
FAutoDeleteAsyncTask
.class FMyTask : public FNonAbandonableTask { friend class FAutoDeleteAsyncTask<FMyTask>; protected: void DoWork() { ... } FORCEINLINE TStatId GetStatId() const { RETURN_QUICK_DECLARE_CYCLE_STAT(FMyTask, STATGROUP_ThreadPoolAsyncTasks); } };
- Create an instance of
FAutoDeleteAsyncTask
for the task.
- Start the task by calling the
StartBackgroundTask
function.
(new FAutoDeleteAsyncTask<FMyTask>())->StartBackgroundTask();
Sleep
The FGenericPlatformProcess::Sleep
function sleeps the current thread for a specific number of seconds. The function uses Stats (FThreadIdleStats
).
0.0 seconds releases the current time slice to let other threads get some attention.
The FGenericPlatformProcess::SleepNoStats
performs the same functionality without Stats.
Implementation
Sleep is implemented with the usleep
function.
On Windows, sleep is implemented using the Sleep
function with a value in milliseconds.
Best Practices
- Do not modify
UObjects
in other threads (without synchronization).
- Do not use
TimerManager
in other threads.
- Do not use Draw Debug Helpers functions in other threads.