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

One more method involves using FGCObjectScopeGuard, ensuring the object remains safe from garbage collection within the asynchronous scope. However, note that the FGCObjectScopeGuard may cause a crash if the object being guarded is forcibly deleted by the garbage collector, which is particularly relevant when exiting the In-Editor Testing mode, that’s why I personally don’t recommend this approach.

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
};