Hello,
After reading some documentation (like https://dev.epicgames.com/documentation/en-us/unreal-engine/object-pointers-in-unreal-engine#t-object-ptr) and engine code, I am trying understand if it’s actually safe to change the value of a UPROPERTY UObject pointer in a worker thread.
Access to said pointer in user-code would obviously be safe as the worker thread is the only one allowed to use it.
I am wondering if there can be issues with the Garbage Collector.
I saw that the Engine does it in “UStaticMesh::SetBodySetup” for example.
I also thought about using a TStrongObjectPtr instead but it doesn’t look safe to create or set to nullptr in a worker thread either.
Thank you.
Hello Philippe,
It can certainly be safe to do so- it depends on what exactly you are trying to do. If you provide more info on what you’re trying to do from the worker thread, I can be more specific.
You are right to be concerned about garbage collection running when you’re dealing with UObjects from outside the game thread. The engine provides an out-of-the-box way to safely prevent garbage collection (GC) from running when you’re doing that: FGCScopeGuard. This scope guard is used in the engine’s async loading system as well as several other systems. There’s a recently-published knowledge base article touching on UObject thread-safety which would likely be of interest to you- you can read that here.
Please let me know if there is anything else I can help you with!
Regards
Hello,
The article you linked is exactly the one that made me dive into UObject thread-safety.
What I am currently trying to do :
Game Thread -> Create new UObject -> Store it in a StrongObjectPtr -> Moves it into a task that will hand it over to a worker thread
Worker Thread -> Receive the StrongObjectPtr and adds it into an array of UObject pointers that is under UPROPERTY
Worker Thread (Tick) -> Do some work on the array that may decide to release some of the stored UObjects by setting the pointer to nullptr
Access to the array is obviously synchronised.
Regards.
Hello Philippe,
To ensure your worker thread can do its job without worrying about objects being garbage collected out from under it, you need to guarantee the following:
- The objects you are accessing from outside the game thread have an unbroken reference chain that reaches back to the root set (like a typical UPROPERTY chain) or have a reference manually added (by TStrongObjectPtr for example)
- Garbage collection does not run while you are accessing these objects or removing references to them from outside the game thread (FGCScopeGuard)
As described, your setup satisfies those guarantees, so I don’t foresee you having any issues with it.
I hope that answers your question!
I also noticed that FWeakObjectPtr::Internal_Pin uses a FGCScopeGuard to create the corresponding TStrongObjectPtr. So I am assuming that its creation and probably destruction are not thread-safe operations.
If I am understanding this correctly, I have to protect the moment I assign a value to the UPROPERTY pointer or when I set it to nullptr with a FGCScopeGuard. Same thing applies even if I use TStrongObjectPtr instead.
Hi Philippe,
Correct for both of your comments. Any time you’re making a change like that which would be visible to the garbage collection system, you’ll want to wrap it in a GC scope guard. You don’t want to be making those changes while GC is in the middle of running.
Hi,
During my testing, I stumbled upon the function FMaterialPSOPrecacheCollectionTask::DoTask that manually calls Reset() on a TStrongObjectPtr thus releasing the object in a worker thread.
This made me take a look at UObjectBase::ReleaseRef and I am asking myself if this is actually thread-safe or not.
FUObjectItem* ObjectItem = GUObjectArray.ObjectToObjectItem(this);
A direct access to the GUObjectArray does seem unsafe but going deeper lead me to
FORCEINLINE FUObjectItem* IndexToObject(int32 Index)
{
check(Index >= 0);
if (Index < ObjObjects.Num())
{
return const_cast<FUObjectItem*>(&ObjObjects[Index]);
}
return nullptr;
}
FUObjectArray::ObjObjects is a FChunkedFixedUObjectArray and comments are suggesting this is thread-safe so I am tempted to think destroying a TStrongObjectPtr in a worker thread is actually possible ?
While this is not directly related to the discussion, this seems like a pointer that is never deleted.
if (bUseBackgroundTask)
{
// Make sure the material instance isn't garbage collected or destroyed yet (create TStrongObjectPtr which will be destroyed on the GT when the collection is done)
TStrongObjectPtr<UMaterialInterface>* MaterialInterface = new TStrongObjectPtr<UMaterialInterface>(Params.Material->GetMaterialInterface());
FGraphEventArray Prereqs;
// Create and kick off the PSO collection task.
TGraphTask<FMaterialPSOPrecacheCollectionTask>::CreateTask(&Prereqs).ConstructAndDispatchWhenReady(MaterialInterface, Params, CollectionGraphEvent, LifecycleID);
// Need to wait for collection task which will be extended during run with the actual async compile events.
OutGraphEvents.Add(CollectionGraphEvent);
}
This is in PSOPrecacheMaterial.cpp and unlike other similar pieces of code, there isn’t a matching FMaterialInterfaceReleaseTask supposed to call the matching delete.
Hi Philippe,
Destroying a TStrongObjectPtr in a worker thread is possible. The ref count on a FUObjectItem is read and written atomically, and GC only cares about whether that ref count is zero or not. Making changes to the ref count during a GC pass should be okay as long as the change does not cause the ref count to switch from zero to nonzero, or vice versa. You’ll need to guarantee that with the design of your multithreaded code.
That memory leak hasn’t been fixed UE5 mainline and I can’t find an existing issue for it, so I’ll file a bug report.
Thank you very much for all the responses.
Happy to help! I’ll close the ticket for now, but feel free to reach out here if you have any additional questions.