Using AsyncTasks
Overview I have been asked about my work with successfully creating FAsyncTasks. It has come up enough in Slack chat that I finally broke down and I want to make a really fantastic article about it...
Overview
I have been asked about my work with successfully creating FAsyncTasks. It has come up enough in Slack chat that I finally broke down and I want to make a really fantastic article about it. Hopefully I will succeed.
What You Will Learn
- What Async Tasks are
- Types of Async Tasks
- How to Declare and Define an Async Task
- How to Instantiate and Run an Async Task
Async Tasks Explained
A so called Async Task is a way of running non-blocking asynchronous code on a thread which is removed from the main 'Game Thread'
Why is this helpful?
Well, if you need to take a long time to complete a task, chunk something up into multiple parts, or generally not interfere with the regular tick and render cycle of a game for any reason, then this is one of the best methods available to you in Unreal Engine.
If you run the same code Synchronously or have it Blocking, it will generally halt your game logic and rendering and cause major performance issues! No fun.
Also, you can vastly increase performance for Asynchronous tasks that can make use of multi-threaded systems.
Types of Async Tasks
The classes that are referred to as Async Tasks are 'friend' classes used in conjunction with FNonAbandonableTask and come in two flavors: FAsyncTask and FAutoDeleteAsyncTask.
FAsyncTask
This task type requires some manual attention in order to stop, or delete the task. Other than that, both flavors are fairly identical.
UE4 Documentation on FAsyncTask
FAutoDeleteAsyncTask
This task type requires zero attention in order to stop, or delete the task. Other than that, both flavors are fairly identical.
UE4 Documentation on FAutoDeleteAsyncTask
How to Declare and Define an Async Task
Although there are multiple ways to accomplish this, I will show you the way in which I handle Async Tasks personally, which should accommodate most uses.
Organization
The method I use to organize my code is to create a C++ Component that handles the types of Tasks I want to create. That way, I can invoke the tasks from Blueprints very easily, and yet have all of the speed of C++. I'm going to focus a lot more on the Async Task code itself, so don't worry about that too much.
The Actual Code
Just below my Component class body in my component's Header file, I add all of my FAsyncTask and FAutoDeleteAsyncTask classes.
They are declared like so:
class FMyTaskName : public FNonAbandonableTask
{
friend class FAutoDeleteAsyncTask;
public:
FMyTaskName(int32 Input, int32 Input2) :
MyInput(Input),
MyInput2(Input2)
{}
protected:
int32 MyInput;
int32 MyInput2;
void DoWork()
{
// Place the Async Code here. This function runs automatically.
}
// This next section of code needs to be here. Not important as to why.
FORCEINLINE TStatId GetStatId() const
{
RETURN_QUICK_DECLARE_CYCLE_STAT(FMyTaskName, STATGROUP_ThreadPoolAsyncTasks);
}
}
The preceding code is valid, although it won't actually perform a task. It is up to you to define what this class actually does.
Also realize that you can and should change the instances of FMyTaskName with something unique and meaningful for each Task.
DoWork() will execute one time automatically once the task is started. I'll go into a little more detail later.
Since it is a full class, feel free to make use of anything else a class can do. Creating sub-functions and the like are all valuable things to do.
Don't forget that you can always '''Forward Declare''' an Async Task if it is needed earlier. Simply declare it like so: "class FMyTaskName;"
How to Instantiate and Run an Async Task
Heading back into the .cpp side of things, I'll show you the proper method for instantiating and executing the actual task itself.
// Instantiate a copy of the actual task, and queue the task for execution with StartBackgroundTask()
(new FAutoDeleteAsyncTask(6,10))->StartBackgroundTask();
As soon as the task is Started, it will be whisked away to the default Thread Pool and queued up for execution. Once a thread is available, it will immediately run the DoWork() function until that function is complete. Once that function is complete, the task will either Auto Delete or become Idle, depending on what type of task was used.
The speed of assigning a Task to a queue is remarkably fast. Other forms of thread management, such as FRunnable, tend to have more delays and overhead surrounding them, and are better suited for certain types of continuous operations.
For regular FAsyncTasks, it is necessary to keep a copy of the pointer around, especially if you want to Delete the object at some point.
For regular FAsyncTasks, there are additional member functions that aid in the use of manipulating them. You can check and see if the task is Idle, if the DoWork function is completed, etc. Read the documentation I linked earlier for more details.