This article addresses the question of how to work with UObjects
in a thread-safe way when dealing with workers, async tasks, thread pools, or whatever else using a non-game thread.
One critical issue to address is the handling of garbage collection. When passing an UObject
, which is not set to root, directly to a background thread, there’s a risk that the garbage collector may silently delete the passed UObject
. Even frequent validity checks of the UObject
(e.g. IsValidLowLevel()
) on a background thread cannot guarantee the object’s survival even moments after the check.
Example of incorrect UObject usage on a background thread
UObject* CreatedObject = NewObject<UObject>();
AsyncTask(ENamedThreads::AnyThread, [CreatedObject]()
{
// Usage of CreatedObject...
// There is uncertainty about the survival of CreatedObject during execution
};
Several methods can resolve this issue:
Place the created UObject into UPROPERTY/TStrongObjectPtr
If you’re confident about the background thread’s lifespan tied to the parent UObject’s existence, placing the created UObject in a separate UPROPERTY
/TStrongObjectPtr
or another strong object reference is a viable solution. However, using regular AsyncTask
functionality doesn’t inherently ensure the parent UObject
’s validity throughout the asynchronous scope, which is important to take in mind.
In more complex scenarios like employing FAsyncTask
with a custom Task
, manual implementation of CanAbandon()
and Abandon()
functions, and designing logic to abandon the task upon UObject
destruction, you can be sure about safety in terms of GC. But this approach isn’t as straightforward as working with regular AsyncThread
. The code below demonstrates an example using regular AsyncTask
, but it’s important to note that, as mentioned earlier, it isn’t entirely foolproof.
Example of placing UObject in UPROPERTY
// .h file
UPROPERTY(Transient)
UObject* CreatedObject; // or "TStrongObjectPtr<UObject> CreatedObject;" without UPROPERTY
// .cpp file
CreatedObject = NewObject<UObject>();
AsyncTask(ENamedThreads::AnyThread, [CreatedObject]()
{
// Usage of CreatedObject...
// It's relatively safer to assume CreatedObject won't be garbage collected in this scope
};
Add UObject to root
To make sure that the object won’t be garbage collected during the whole execution of your background scope, you can control the whole lifetime manually, by directly adding the needed object to the root, and removing it once it’s ready for being destroyed by the garbage collector. In this case, you should pay attention to the object’s lifetime manually, and avoid forgetting to remove it from root, because otherwise, the object will live forever.
Example of creating UObject with adding to root
UObject* CreatedObject = NewObject<UObject>();
CreatedObject->AddToRoot();
AsyncTask(ENamedThreads::AnyThread, [CreatedObject]()
{
// Usage of CreatedObject...
// Ensures CreatedObject won't be garbage collected in this scope
AsyncTask(ENamedThreads::GameThread, [CreatedObject]()
{
// If, during this stage, you don't want the object to avoid garbage collection, remove it from the root
CreatedObject->RemoveFromRoot();
};
};
Create UObject on a background thread itself
Another approach is to create the needed UObject
directly on a background thread. When created in a non-game thread, the object automatically receives an Async object flag (in UObjectBase::AddObject
), listed under GarbageCollectionKeepFlags
, which prevents the object from being garbage collected (see MarkObjectsAsUnreachable
in GarbageCollection.cpp
, and IsNonGCObject
in ReferenceChainSearch.cpp
). And once the object is allowed to be garbage collected, you should clear the Async
object flag.
Example of creating UObject on background thread
AsyncTask(ENamedThreads::AnyThread, [CreatedObject]()
{
UObject* CreatedObject = NewObject<UObject>();
// Not inherently necessary since the Async flag is automatically added once the object is created in the background thread, but just for explicitness
CreatedObject->SetInternalFlags(EInternalObjectFlags::Async);
// Usage of CreatedObject...
// Ensures CreatedObject won't be garbage collected in this scope
AsyncTask(ENamedThreads::GameThread, [CreatedObject]()
{
// If, during this stage, you don't want the object to avoid garbage collection, clear the Async flag
CreatedObject->ClearInternalFlags(EInternalObjectFlags::Async);
};
};
That is essentially a similar approach to when adding to root (->AddToRoot()
) and removing from root (->RemoveFromRoot()
), but the Async flag is better suited when the object is created asynchronously, which is the case when creating an object inside background threads
FGCObjectScopeGuard
FGCObjectScopeGuard
can be used to protect a UObject from being garbage collected within a scope. However, it is not safe to use on a background thread, due to a potential race condition with the garbage collector. If the guard goes out of scope while the garbage collector is processing the object, the game may crash. This risk exists both in the editor and in packaged builds. For this reason, I do not recommend using FGCObjectScopeGuard
in multithreaded contexts.
Example of FGCObjectScopeGuard
UObject* CreatedObject = NewObject<UObject>();
AsyncTask(ENamedThreads::AnyThread, [CreatedObject]()
{
FGCObjectScopeGuard CreatedObjectGuard(CreatedObject);
// Usage of CreatedObject...
// Ensures CreatedObject won't be garbage collected in this scope
};