How do I setup FRunnable threads that notify game thread on completion?

I’m trying to make my own “thread pool” where I would have permanent couple of threads that will be used to generate mesh data for my grid chunks in my game and this is my current setup:

UpdateChunkThread.h

struct FCompletedThreadData
{
	uint32 Result = 1;
	FVoxelMeshSectionData MeshData;
};

class FUpdateChunkThread : public FRunnable
{
	friend class UChunkUpdateQueueComponent;

public:

	DECLARE_EVENT(FUpdateChunkThread, FOnCompletedEvent);

	FOnCompletedEvent OnCompleted;

public:

	FUpdateChunkThread(AChunk* NewChunk);
	virtual uint32 Run() override;
	virtual void Stop() override;
	virtual void Exit() override;
	virtual ~FUpdateChunkThread() override;

	void Start();
	FCompletedThreadData GetData();

private:

	TAtomic<bool> mb__Occupied = false;
	FString Name;
	FUpdateMeshInformation m__UpdateMeshInformation;
	FCompletedThreadData m__CompletedThreadData;
	FCriticalSection m__Mutex;
	FRunnableThread* m__Thread;
	TArray<FBlock> m__Blocks;
};

VoxelMeshSectionData.h

...
USTRUCT(BlueprintType)
struct FVoxelMeshSectionData
{
	GENERATED_BODY();

	FVoxelMeshSectionData()
	{
		
	}

	UPROPERTY(BlueprintReadWrite)
	TArray<FVector> Positions;

	UPROPERTY(BlueprintReadWrite)
	TArray<int> Triangles;

	UPROPERTY(BlueprintReadWrite)
	TArray<FVector2D> UVs;

	UPROPERTY(BlueprintReadWrite)
	TArray<FVector> Normals;

	UPROPERTY(BlueprintReadWrite)
	TArray<FProcMeshTangent> Tangents;
};

UpdateChunkThread.cpp

FUpdateChunkThread::~FUpdateChunkThread()
{
	if (m__Thread)
	{
		m__Thread->Kill();
		delete m__Thread;
	}

	OnCompleted.Clear();
	m__Thread = nullptr;
}

void FUpdateChunkThread::Start()
{
	m__Thread = FRunnableThread::Create(this, *Name);
}

FCompletedThreadData FUpdateChunkThread::GetData()
{
	m__Mutex.Lock();
	FCompletedThreadData CompletedThreadData = m__CompletedThreadData;
	m__Mutex.Unlock();

	return CompletedThreadData;
}

FUpdateChunkThread::FUpdateChunkThread(AChunk* NewChunk)
{
	mb__Occupied = true;
	Name = FString("Chunk_" + FString::FromInt(NewChunk->m__ChunkLocationData.IndexInGrid));
	const FGridData GridData = NewChunk->GetContainingGrid()->GetGridData();
	m__UpdateMeshInformation.ChunkSize = GridData.ChunkSizeInBlocks;
	m__UpdateMeshInformation.BlockSize = GridData.BlockSize;
	m__UpdateMeshInformation.UnscaledBoxExtent = NewChunk->m__OverlapCollision->GetUnscaledBoxExtent();
	m__Blocks = NewChunk->__CopyRelevantBlocksForUpdate();}

uint32 FUpdateChunkThread::Run()
{
	FCompletedThreadData CompletedThreadData;
	UGridMeshUtilities::MarchingCubes(m__Blocks, m__UpdateMeshInformation, CompletedThreadData.MeshData);
	CompletedThreadData.Result = 0;

	m__Mutex.Lock();
	m__CompletedThreadData = CompletedThreadData;
	m__Mutex.Unlock();

	return 0;
}

void FUpdateChunkThread::Stop()
{
	FRunnable::Stop();
}

void FUpdateChunkThread::Exit()
{
	FRunnable::Exit();
	mb__Occupied = false;

	AsyncTask(ENamedThreads::GameThread, [this]()
	{
		OnCompleted.Broadcast();
	});
}

ChunkUpdateQueueComponent.h

class UChunkUpdateQueueComponent : public UActorComponent
{
	void __EnqueueChunk(AChunk* ChunkToUpdate);
	void __CheckThreads();

	bool __IsThereWork();

	TArray<AChunk*> m__QueuedChunks;
	TArray<FUpdateChunkThread*> m__PermanentThreads;
}

ChunkUpdateQueueComponent.cpp

void UChunkUpdateQueueComponent::__EnqueueChunk(AChunk* ChunkToUpdate)
{
	m__QueuedChunks.AddUnique(ChunkToUpdate);
	__CheckThreads();
}

void UChunkUpdateQueueComponent::__CheckThreads()
{
	for (int i = 0; i < m__PermanentThreads.Num(); i++)
	{
		FUpdateChunkThread* UpdateChunkThread = m__PermanentThreads[i];
		if (UpdateChunkThread)
		{
			if (UpdateChunkThread->mb__Occupied)
				continue;
			
			delete m__PermanentThreads[i];
			m__PermanentThreads[i] = nullptr;
		}

		if (!__IsThereWork())
			return;

		AChunk* Chunk = m__QueuedChunks.Pop();
		UpdateChunkThread = new FUpdateChunkThread(Chunk);
		m__PermanentThreads[i] = UpdateChunkThread;
		UpdateChunkThread->OnCompleted.AddLambda([this, Chunk, UpdateChunkThread]()
		{
			const FCompletedThreadData CompletedThreadData = UpdateChunkThread->GetData();
			if (CompletedThreadData.Result == 0)
				Chunk->_UpdateMesh(CompletedThreadData.MeshData);
			__CheckThreads();
		});
		UpdateChunkThread->Start();
	}
}

bool UChunkUpdateQueueComponent::__IsThereWork()
{
	return !m__QueuedChunks.IsEmpty();
}

There is only ever one of that component in the world and it’s attached to the grid.

When a chunk needs to be updated I use UChunkUpdateQueueComponent ::__EnqueueChunk() and dispatch a thread to deal with the calculations while of course copying over the blocks from the chunk as they need to be modifiable in the game thread while this calculation is running in the background. The problem is when OnCompleted.Broadcast() gets called it crashes the editor/game.

The flow of the process of updating a chunk is like this:
Chunk needs to update → enqueue to ChunkUpdateQueueComponent → if any thread is currently not occupied (not working) dispatch it to calculate for the top most chunk from the queue array (I use array but it works in queue fashion) → the relevant information for the thread gets set/copied over → the ChunkUpdateQueueComponent creates a lambda and subscribes it to the event OnCompleted of FUpdateChunkThread → the thread starts calculating and sets that it’s occupied so other chunks’ meshes cannot be calculated using this thread → when it’s done calculating it creates an AsyncTask to the GameThread to notify that it has completed its job → Lambda gets called and it calls to update the Chunk with the new mesh data and calls ChunkUpdateQueueComponent to check if another chunk needs processing or if threads need to be cleaned out and reset for the next time a chunk needs updating.

So far I’ve tried a couple different things and I’ve gotten many different crashes, but it probably boils down to me not understanding how threads work. The current implementation also doesn’t work, it crashes when the broadcast gets called inside FUpdateChunkThread. I’ve tried using a delegate with one param to pass the data directly using an argument, but it crashes when trying to copy the data over. If that doesn’t crash, meaning I’m using a delegate with no param, it crashes when trying to broadcast.

Also if there’s a better way of writing the thread, while keeping in mind I need to give the thread the data to wok with and it generates data back and gives need to give it back to the game thread and also notify when it’s done, I would appreciate the tips.

Also I read on the wiki that using Tasks is not recommended for long calculations where you can freeze the thread for a couple of seconds as they can execute synchronously or asynchronously depending on what the engine decides, hence why I’m using FRunnable.

try calling the asynctask before the FRunnable::Exit() in you exit override

I tried that, doesn’t work.