Hello!
I have a bug that I’m attempting to squash, and I may have uncovered unexpected behavior.
A couple years ago, I asked about this and this was the response:
Tasks scheduled through the Schedule_ api will schedule a single task that performs all the logic, whereas Fork_ tasks will schedule one task per allocation that matches the filter. We use Fork tasks for things like curve evaluation (where there are almost no dependencies, and we need to unblock downstream tasks asap).
I currently have a system that has 2 tasks in it:
- FGatherMyObjects
- FApplyOffsets
They’re defined something like this inside OnSchedulePersistentTasks
// Step 1: Get all my objects
FTaskID GatherTask = FEntityTaskBuilder()
.Read(BuiltInComponents->EvalTime)
.Read(BuiltInComponents->BoundObject)
.SetParams(FTaskParams(TEXT("Gather My Objects")).ForcePrePostTask())
//.SetDesiredThread(Linker->EntityManager.GetGatherThread()) // I noticed this was on a bunch of other Systems, and this does seem to force the same thread
.Schedule_PerAllocation<FGatherMyObjects>(&Linker->EntityManager, TaskScheduler, &MyArrayOfObjects);
// Make sure we're looking at a ComponentTransform and that it's set to absolute blend.
FEntityComponentFilter Filter;
Filter.All({ TracksComponents->ComponentTransform.PropertyTag, BuiltInComponents->Tags.AbsoluteBlend });
Filter.None({ BuiltInComponents->BlendChannelOutput });
// Step 2: Apply Offset
FTaskID ApplyTask = FEntityTaskBuilder()
.Read(BuiltInComponents->BoundObject)
.WriteOptional(BuiltInComponents->DoubleResult[0])
.WriteOptional(BuiltInComponents->DoubleResult[1])
.WriteOptional(BuiltInComponents->DoubleResult[2])
.WriteOptional(BuiltInComponents->DoubleResult[3])
.WriteOptional(BuiltInComponents->DoubleResult[4])
.WriteOptional(BuiltInComponents->DoubleResult[5])
// Must contain at least one double result
.FilterAny({ BuiltInComponents->DoubleResult[0], BuiltInComponents->DoubleResult[1], BuiltInComponents->DoubleResult[2],
BuiltInComponents->DoubleResult[3], BuiltInComponents->DoubleResult[4], BuiltInComponents->DoubleResult[5] })
.CombineFilter(Filter)
.Fork_PerAllocation<FApplyOffsets>(&Linker->EntityManager, TaskScheduler, &MyArrayOfObjects);
TaskScheduler->AddPrerequisite(GatherTask, ApplyTask);
FGatherMyObjects looks something like this:
struct FGatherMyObjects
{
TArray<UObject*>* MyArrayOfObjects;
void PreTask() const
{
MyArrayOfObjects->Reset();
}
void ForEachAllocation(const FEntityAllocation* Allocation, TRead<FFrameTime> EvalTimes, TRead<UObject*> BoundObjects) const
{
const int32 NumAllocations = Allocation->Num();
for (int32 Index = 0; Index < NumAllocations; ++Index)
{
// Evaluate the characterMeshOffset section at the current time to make sure it's enabled
bool ShouldOffset = false;
// -- Logic here to decide if we should offset this object --
if (ShouldOffset)
{
MyArrayOfObjects->Add(BoundObjects[Index]);
}
}
for (UObject* Object : *MyArrayOfObjects)
{
// Some logging here
// This caused the Array iterator to tell me that the size changed while iterating!
}
}
};
I was getting odd behavior when applying the offset and noticed that the array didn’t seem to always be properly filled, despite logs telling me that it had been. I then added the for loop for debugging; I immediately hit an error saying that the size of the array had changed.
I set breakpoints and noticed that this task was being run in two threads (Background Worker #0 and Foreground Worker #0). The end result is that I’m adding to the array from two different threads.
So now to my question: Am I using Schedule incorrectly? What should I be doing differently? If this is indeed expected, how should I gather?
I think I can work around this by forcing this task to run on the Game/Gather Thread, but that seems antithetical to the way “Schedule_” is supposed to work.
Thanks!
-Nathaniel
[Attachment Removed]