Duplicate Actors Share BeginPlay Logic Using Async

Set up is super simple. I have the code below within an actor class. I create a BP based on this and ctl+d 9 other actors. When I press play PIE each of the 10 actors log their unique name but they do it at the same time. I’d expect with the RandRange they would be running in their own thread printing at different times.

Again, actor names come back unique but the sleep time is identical. I’m probably missing something obvious but I’ve been stumped on this for a bit so I was hoping someone else knew what I was doing wrong.

What I want is for each duplicated actor to spawn it’s own thread and print to the log over
a random specified range time. Thanks for any pointers!

void ACPP_ThreadActor::PrintSomething(FString ActorName)
{
	while(bAsyncRunning)
	{
		FPlatformProcess::Sleep(FMath::RandRange(1.f,5.f));
		UE_LOG(LogTemp, Warning, TEXT("[%s]Printing STUFF!"), *ActorName);
	}
}

// Called when the game starts or when spawned
void ACPP_ThreadActor::BeginPlay()
{
	Super::BeginPlay();

	auto Result = Async(EAsyncExecution::Thread, [this]()
	{
		PrintSomething(GetName());
	});
}

FMath::RandRange calls the c function rand() under the hood (via FMath::RandHelper, FGenericPlatformMath::FRand and FGenericPlatformMath::Rand). Unfortunately rand() isn’t thread-safe which might be the problem here.

rand() is not guaranteed to be thread-safe.
rand - cppreference.com.

Since you probably want to avoid having to lock access to the random numbers you may want to look into using a local FRandomStream instance instead. You can initialize/seed those differently outside the thread.

Legend, thank you! I wasn’t thinking thread safety here since each duplicated object should have encapsulated logic. Each object, each new thread, each new call to RandRange. I wasn’t even looking in the underlying logic of Rand. Seriously appreciate it!

In case someone in the future wants to see the solution.

void ACPP_ThreadActor::PrintSomething(FString ActorName, int RandomTime)
{
	while(bAsyncRunning)
	{
		FPlatformProcess::Sleep(RandomTime);
		UE_LOG(LogTemp, Warning, TEXT("[%s]Printing STUFF! [%i]"), *ActorName, RandomTime);
	}
}

// Called when the game starts or when spawned
void ACPP_ThreadActor::BeginPlay()
{
	Super::BeginPlay();

	FRandomStream* RandomTime = new FRandomStream(NAME_None);
	
	auto Result = Async(EAsyncExecution::Thread, [this, RandomTime]()
	{
		PrintSomething(GetName(), RandomTime->RandRange(1,5));
	});
}

The behaviour is slightly different with the new code you posted. You only call RandRange once and then reuse that value in each iteration of the while loop. You’d need to pass the FRandomStream* RandomTime to PrintSomething or make the RandomStream a (UPROPERTY) member of the actor (if I remember correctly that should automatically seed each RandomStream for each instance independently).

And since you never delete RandomTime there is a memory leak.

Blockquote
You’d need to pass the FRandomStream* RandomTime to PrintSomething

That’s exactly what I did in the posted code. I created a random value, passed that value to PrintSomething. Then, PrintSomthing prints at intervals of that value (each object has unique interval time).

Blockquote
or make the RandomStream a (UPROPERTY ) member of the actor (if I remember correctly that should automatically seed each RandomStream for each instance independently).

This is probably what I should have done but this was quick Async testing. Although, the RandomStream knowledge is valuable and I wasn’t aware of it before!

Blockquote
And since you never delete RandomTime there is a memory leak.

This is a good point. I’m not using the posted example for anything serious but for completeness absolutely. Anyone in the future should remember to delete/nullptr the pointers.

Thanks again!

For the record: No, you passed an integer value to that function, which means that the Sleep will be called with the same number each time under a single while loop. So the code flow is like this:

  • create a random stream
  • get a random number between 1-5 (let’s say 2)
  • call PrintSomething with this value (2)
  • in the while loop sleep for this time (2)

What @UnrealEverything meant by passing the FRandomStream to the PrintSomething function is that instead of an int parameter, you have the FRandomStream* as a parameter, and call RandRange inside your while loop.

Setting it to nullptr is not enough, this is not a managed (garbage collected) ecosystem. A modern approach is to use a managed pointer to automatically delete the object once it’s going out of scope.

1 Like

This topic was automatically closed 30 days after the last reply. New replies are no longer allowed.