Thread safe dictionary

Godot Version

4.4.1

Question

I am trying to create a thread safe dictionary that does a heavy computation for some arguments and stores the result against those arguments.

In this case, based on enemy’s speed and jump speed, building a navigation graph is the computation.

There can be multiple enemies for a particular [speed, jump_speed]. I want other enemies to wait on get_graph if someone else already triggered the process with the same parameters.

I tried with Signal() but I was not able to make it work. Even by passing parameters to Signal(). Turns out I can only use signal keyword. Spent my whole day tried to make it working.
Can use a Callable here in some way? How do others handle this?

nav_manager.gd

var graphs: Dictionary[Array, NavGraph] = {}
var pending_graphs: Dictionary[Array, Signal] = {}
func get_graph(speed: float, jump_speed: float):
	var key = [speed, jump_speed]
	# If the graph exists, return it
	var navgraph: NavGraph = graphs.get(key)
	if navgraph:
		return navgraph
	
	# If the graph is in pending state
	if pending_graphs.has(key):
		await pending_graphs.get(key)
		return graphs[key]
	
	var graph_built = Signal()
	pending_graphs[key] = graph_built

	# Create the graph
	var new_graph = build_graph(speed, jump_speed)

	graphs[key] = new_graph
	pending_graphs.erase(key)
	return new_graph 


enemy_controller.gd
...
var navgraph: NavGraph
func _ready() -> void:
	navgraph = await navmesh_manager.get_graph(speed, jump_speed)
...


Where’s the thread?

1 Like

The await keyword waits for coroutines not threads.

I considered multiple enemy controllers to be the threads, calling this coroutine. The reason Signal() didn’t work is because I didn’t do add_user_signal. I thought Signal(obj, name) wil create a signal but rather it kind-of refers to the existing one. It’s working fine now.

There are no threads if you haven’t created any using the Thread object. Everything, including all coroutines will run in the main thread.

Are you sure? You’re doing several very strange things in get_graph(), like using an array as a dictionary key wrongly assuming it will be used “by value”. Btw, where do you emit the awaited signal?

Thanks for correcting me. I was unnecessarily worried about concurrent calls. The following is working now.

func get_graph(speed: float, jump_speed: float, size: Vector2 = Vector2(0,0)):
	var key = Vector2(speed, jump_speed)
	# If the graph exists, return it
	var navgraph: NavGraph = graphs.get(key)
	if navgraph:
		return navgraph
	# Otherwise create it
	navgraph = _build_graph(speed, jump_speed)
	graphs[key] = navgraph
	return navgraph

I emitted the signal just before erase the key from pending_graph (missed it here somehow). Corrected that Vector2 earlier.

I used await in build_graph because get_graph was getting called before some preprocessing in _ready(). I thought it’s concurrency issue. Now I moved my nav manager up in the scene, it works now. no await in enemy as well.

I am new to godot. Any resources you would suggest?

Much better. Yeah, you don’t need coroutines for this at all. Simply either create the graph when requested or return the cached one if it was already created before.

The key may still cause bugs due to float imprecision. Better to use Vector2i instead of Vector2:

const granularity := 100.0
var key := Vector2i(roundi(speed * granularity), roundi(jump_speed * granularity))

Official docs are quite good.

2 Likes