Shared Pointer / Async Crash from Quitting Early

I’m offloading a bunch of terrain generation to an AsyncTask.

I have a free function with this signature:

namespace GenerateMeshData

{

    void CreateMeshData(TSafeSharedPtr<FProceduralChunkSharedData> SharedData,

                        FProceduralChunkData ChunkData,

                        FVector ActorLocation);
}

Where a TSafeSharedPtr<T> is just

template <typename T>
using TSafeSharedPtr = TSharedPtr<T, ESPMode::ThreadSafe>;

I’m spawning a bunch of actors with ProceduralMeshes, and each one that spawns creates a new task to generate its mesh with

	AsyncTask(ENamedThreads::Type::AnyBackgroundThreadNormalTask, [=, Location = GetActorLocation()]()
			  { GenerateMeshData::CreateMeshData(SharedData, ChunkData, Location); });

SharedData here is simply a member of the Actor. It is a TSafeSharedPtr to a plain struct that contains TArrays of primitives (not UObjects) and a bool that is initialized to false and becomes true when the calculation is done. ChunkData is just a struct of settings for the generation, mostly ints.

This code all works; the terrain generation takes place and I have a manager Actor that simply spawns these in and out of the game as the player runs around. I can run around in infinite rolling hills to my heart’s content. However I have some sort of problem with the TSafeSharedPtr, if I quit the game early I get an exception in the destructor of the pointer, right where the CreateMeshData function returns.

It seems to me that perhaps the object pointed to by the pointer is getting freed before this thread finishes, but that shouldn’t be possible with threadsafe reference counting . . . So I’m probably wrong and that’s not what’s happening.

Any help is greatly appreciated. Doubly so if there is a much better way to do this.

More code, in the hopes that it will help fix this . . .

The declaration of the SharedData member:

TSafeSharedPtr<FProceduralChunkSharedData> SharedData;

The initialization:

AProceduralChunk::AProceduralChunk()
{
	// Set this actor to call Tick() every frame.  You can turn this off to improve performance if you don't need it.
	PrimaryActorTick.bCanEverTick = true;

	ProceduralMesh = CreateDefaultSubobject<UProceduralMeshComponent>(TEXT("ProceduralMesh"));
	RootComponent = ProceduralMesh;

	// Enable async cooking to offload the mesh collision generation to a separate thread
	ProceduralMesh->bUseAsyncCooking = true;

	SharedData = MakeShared<FProceduralChunkSharedData, ESPMode::ThreadSafe>();
}

OnConstruction, where I start the task.

void AProceduralChunk::OnConstruction(const FTransform &Transform)

{

    if (ChunkMasterMaterial)

    {

        ChunkMaterialInstance = UMaterialInstanceDynamic::Create(ChunkMasterMaterial, this);

    }

    AsyncTask(ENamedThreads::Type::AnyBackgroundThreadNormalTask, [=, Location = GetActorLocation()]()

              { GenerateMeshData::CreateMeshData(SharedData, ChunkData, Location); });

}

And then finally Tick and the function where I check if the data is done and ready to be used:

void AProceduralChunk::FinalizeMeshData()
{
	if (ProceduralMesh)
	{
		if (ProceduralMesh->GetNumSections() == 0)
		{
			if (SharedData->bIsMeshDataGenerated)
			{
				TArray<FColor> EmptyColors;
				ProceduralMesh->CreateMeshSection(0, SharedData->Vertices, SharedData->Triangles, SharedData->Normals, SharedData->UVs, EmptyColors, SharedData->Tangents, true);
				ProceduralMesh->SetMaterial(0, ChunkMaterialInstance);
				bIsReady = true;
			}
		}
	}
}

// Called every frame
void AProceduralChunk::Tick(float DeltaTime)
{
	Super::Tick(DeltaTime);
	if (!bIsReady)
	{
		FinalizeMeshData();
	}
}

From my understanding of threadsafe shared pointers, the task should hold onto its copy of the pointer until it is completely finished, preventing the pointed-at data from becoming invalid?

While you are copying the SharedPtr into the CreateMeshData function you are NOT actually copying it into the lambda passed into AsyncTask. So until that happens there should only be a single reference count from the AProceduralChunk. If that Actor is getting destroyed (especially since you said you’re spawing them out as the player runs around) the lambda’s captured this pointer would be dangling. I’d suggest you get rid of the implicit by copy capture in the lambda and explicitly copy the required data. You may also want to capture a WeakObjectPtr of the owning Actor and check whether that Actor is still valid before calling CreateMeshData. Not sure what happens with the tasks that were already created when you quit the game but Actors and other UObjects are definitely being destroyed.

From Lambda expressions (since C++11) - cppreference.com

The current object (*this) can be implicitly captured if either capture default is present. If implicitly captured, it is always captured by reference, even if the capture default is =. The implicit capture of *this when the capture default is = is deprecated. (since C++20)

Minimal example of what’s happening with the capture default:

Been using c++ for over a decade and there’s always a new footgun to learn about . . .

Thank you very much! The reference-I-thought-was-a-copy was in fact the problem.