Repository: godot
Issue: Object reference will persist in memory if touched by a thread.
Labels: bug,topic:gdscript,needs testing

### Godot version

4.1.1.stable

### System information

Windows 10 - Godot 4.1.1.stable - +forward

### Issue description

I've made a script for a procedural generation of 3d meshes and the data used for these meshes is stored in objects defined with this class, the job is done using multiple threads.
```gdscript
class CompiledChunk extends RefCounted:
	# Structure about a square portion of the map that has been generated.
	
	var position: Vector3 = Vector3.ZERO;
	# Global position in the world where the chunk should be placed.
	
	var mesh: ArrayMesh = null;
	# The visible mesh of the chunk.
```
Figure 1. The object definition that store the generated piece.

### Steps to reproduce

I have this function that has the job to identify the pieces required to generate and after all information has been collected it will create new instances of `CompiledChunk` and then dump all the work to threads wich will execute `_compile_chunk_mesh` where all the magic happens.
Since working with threads is async I made an helper class to define signals, called when generating the mesh.

```gdscript
class ThreadChunkPromise extends RefCounted:
	
	var threads: Array[Thread];
	# The threads working on this promise.
	
	signal on_chunk_generated (chunk: CompiledChunk);
	# Called after a chunk has been compiled.
	
	signal on_finished ();
	# Called after all threads finished their job.
```
Figure 2. The helper class to handle async signals wich are called with `call_deferred`.

Each variable of the threads are local, the only global variable each thread grab is `chunks` wich is a list of `CompiledChunk` and right where the thread grab the object the shit happen.
The monitor of the objects count increases, when I clear the chunks and re-call the function the object count continue to increase.
I have another function similar to this but it does not use threads and this problem does not happen.
```gdscript
func chunk_thread_all (max_threads: int = 1) -> ThreadChunkPromise:
	"""
	Will compile all missing chunks using threads, you can specify how many
	of them to use. It returns an object that will call signals once the job is done.
	
	If this job was already started from a previus call it will return the previous
	promise wich may not contain the same amount of max threads.
	
	It will return null if the amount of threads is invalid.
	"""
	
	if not is_instance_valid(map):
		push_error('Compile a map first, no chunks have been generated.');
		return null;
	
	if max_threads <= 0:
		push_error('Must at least use 1 thread to build chunks.');
		return null;
	
	if has_meta('_chunk_promise'):
		push_warning('Already generating chunks with threads, returning previus promise.');
		return get_meta('_chunk_promise');
	
	
	# Find all the missing chunk to compile.
	var x: int = 0;
	var y: int = 0;
	var ccunks: = PackedInt32Array();
	
	while y < hchunks:
		var i: int = x + (y * wchunks);
		
		if not is_instance_valid(chunks[i]):
			var cc: = CompiledChunk.new();
			chunks[i] = cc;
			ccunks.append(x);
			ccunks.append(y);
		
		x += 1;
		if x >= wchunks:
			x = 0;
			y += 1;
	
	
	# Make async promise.
	var promise: = ThreadChunkPromise.new();
	set_meta('_chunk_promise', promise);
	
	var _finish: = func () -> void:
		remove_meta('_chunk_promise');
		promise.on_finished.emit();
	
	if ccunks.size() <= 0:
		push_warning('All chunks are already compiled.');
		_finish.call_deferred();
		return promise;
	
	# Split the job for all threads.
	var work_amount: int = (ccunks.size() / 2) as int;
	
	# Less chunks than avaiable threads, reduce max threads to chunk amount.
	if work_amount < max_threads:
		push_warning('There are not enough chunks to split between {0} threads. Max threads are reduced to {1}.'.format([max_threads, work_amount]));
		max_threads = work_amount;
	
	# How many chunks for each thread.
	var pieces: int = ceil(work_amount as float / max_threads);
	
	# called after a chunk has been completed.
	var _compiled_chunk: = func (cc: CompiledChunk) -> void:
		promise.on_chunk_generated.emit(cc);
	
	# called after a thread has finished his job.
	var _finished_thread: = func (thread: Thread) -> void:
		
		if thread.is_alive():
			thread.wait_to_finish();
		promise.threads.erase(thread);
		
		# No more threads? Job has been completed.
		if promise.threads.size() <= 0:
			_finish.call();
	
	var _job: = func (thread: Thread, offset: int) -> void:
		
		for piece in range(pieces):
			var idx: int = offset + (piece * 2);
			
			if idx >= ccunks.size():
				break;
			
			var cx: = ccunks[idx];
			var cy: = ccunks[idx + 1];
			var cc: = chunks[cx + (cy * wchunks)];	# HUGE PROBLEM HERE. MEMORY LEAK.
			# For some reason, inside a thread chunks will persist in memory forever.
			
			_compile_chunk_mesh(cc, cx, cy);
			_compiled_chunk.call_deferred(cc);
		
		_finished_thread.call_deferred(thread);
	
	# Start the job for each thread.
	for thread_i in range(max_threads):
		var thread: = Thread.new();
		
		# It should always happen, right?
		if thread.start(func() -> void: _job.call(thread, (thread_i * 2) * pieces)) == OK:
			promise.threads.append(thread);
	
	return promise;
```
Figure 3. The function with the problem.

So, the threads increase the reference count of `CompiledChunk` wich will never be cleared from memory.

I checked my code 100 times, the error is only in this function and. The threads increase the object reference but not decrease it.
Am I doing a mistake with threads? I know how interacting with globals using them can cause problems, but here almost everything is local variable wich iirc should be ok with threads.

### Minimal reproduction project

The script I have is a group project wich I can't share all of it, I think no minimal reproduction is needed because from the code I show is very obvius what is happening.