Unlocking my Mutex doesn't seem to work.

:information_source: Attention Topic was automatically imported from the old Question2Answer platform.
:bust_in_silhouette: Asked By navett52

Hey all, I’m attempting to work with threads and I’ve gotten to what I believe is a pretty good piece of code. Here it is below:

var mutex = Mutex.new()

func _ready():
	Start()
	pass

func Start():
	SpawnThread()
	SpawnThread()
#	SpawnThread()
#	SpawnThread()

func SpawnThread():
	var thread = Thread.new()
	
	var data = {
		"thread": thread,
		"lock": mutex
	}
	
	thread.start(self, "threadFunction", data)

func threadFunction(data):
	while true:
		print("Thread " + str(data["thread"].get_id()) + " has started the loop!")
		if data["lock"].try_lock() == 0:
			print("Lock is unlocked! Lock is next.")
			data["lock"].lock()
			print("Thread " + str(data["thread"].get_id()) + " has the lock!")
			print("Eating...")
			yield(get_tree().create_timer(9), "timeout")
			print("Done eating!")
			print("Releasing lock")
			data["lock"].unlock()
		
		print(str(data["thread"].get_id() + " is waiting for lock..."))
		yield(get_tree().create_timer(5), "timeout")

This works fantastically the first iteration. Normally Thread1 get’s the lock and starts “eating” (just waiting to simulate some work being done) Thread2 goes through once and waits, then Thread1 finishes right as Thread2 is done waiting. You would think Thread2 would get the lock and go on it’s merry way, but it’s once the lock is unlocked, both threads just keep waiting and no one acquires the lock again. I am stumped. Does the mutex get removed after the first use? I would imagine I could keep locking and unlocking the same mutex over and over again.

UPDATE: So I have confirmed that unlock just doesn’t seem to be unlocking the mutex by printing out the value of mutex.try_lock(). Is there a reason it’s doing this? Is there a fix?

:bust_in_silhouette: Reply From: omggomb

I’m not sure but think it’s because of using the scene tree for the timer. The docs say it’s not thread safe and you should use call_deferred() to interact with it. Link to the docs.

If I create a semaphore for each thread so that I can use the main thread to control their waiting and replace the yield after “Eating…” with the following:

# During thread creation
var data = {
#...
    semaphore: Semaphore.new()
}

#After eating
call_deferred("_wait_for_timer", data["semaphore"])
data["semaphore"].wait()
#...

func _wait_for_timer(semaphore):
    yield(get_tree().create_timer(9), "timeout")
    semaphore.post()

it seems to work. But I’m not sure where the culprit lies.

Thank you for the reply! I’ll definitely crack into this when I have some time and see what I can come up with using your suggestion. Could you either comment or add the documentation link where it says that? I’ve been looking at the documentation and seemed to have missed that… so I’d love to know where it is.

navett52 | 2020-04-10 16:29

I’m trying to work in your idea, but I’m a bit confused. Are you replacing the mutex with a semaphore, or are you adding in a semaphore in addition to the mutex?

navett52 | 2020-04-10 23:03

EDIT: I’m not sure anymore whether call_deferred actually executes stuff on the main thread, but the solution is still correct. I’ll see if I can find an answer to that.
EDIT 2: The page about servers sort of hints to that by mentioning a MessageQueue. This is what call_deferred internally uses, so I figure my assumption is correct, since it mentions that the queue is flushed by the scene tree during idle time, and the scene tree most likely runs on the main thread. Relevant link about servers.

Sorry for not adding the doc link, I was lazy :p. I edited my answer to include it.

The semaphore is aditionally to the mutex. But keep in mind that this whole thing is just to simulate work being done, so the real world use of this setup depends. Here is the entire relevant code with some more comments, though I left out the try_lock line:

func SpawnThread():
	var thread = Thread.new()

	var data = {
		"thread": thread,
		"lock": mutex,
		"sem": Semaphore.new() # One semaphore for each thread
	}
	thread.start(self, "threadFunction", data)

func threadFunction(data):
	while true:
		print("Thread " + str(data["thread"].get_id()) + " has started the loop!")
		
		data["lock"].lock()	
		print("Thread " + str(data["thread"].get_id()) + " has the lock!")
		print("Eating...")
		# Don't want get_tree().create_timer to be executed in the thread
		# so use call_deferred which will call the relevant
		# function from the main thread
		# Hand over this thread's semaphore as a way to be notified when the timer 
		# has timed out.
		call_deferred("_wait", data["sem"])
		# Wait for the signal that will be sent by the main thread once the timer
		# has timed out
		data["sem"].wait()
		
		print("Done eating!")
		print("Releasing lock")
		data["lock"].unlock()
		print(str(data["thread"].get_id() + " is waiting for lock..."))

func _wait(sem):
	# This gets called from our threads, but it is executed on the main thread
	# due to call_deferred, so it's save to interact with the scene tree
	# Use yield to wait for the timer to time out
	yield(get_tree().create_timer(randf() * 3), "timeout")
	# Now notify the thread that has called this function by using the thread's
	# own semaphore
	sem.post()

omggomb | 2020-04-11 11:19

Thank you very much! I tried code adding the semaphore to the mutex and replacing the mutex, I swear I tried something exactly like this, but I must have gotten a line or two wrong somewhere. I have realized I definitely need to read up on semaphores a bit. Anyway, I greatly appreciate your help! This code works.

A clarifying question, we’re only using call_deferred here because of using the scene tree to create that timer that simulates work, right? Would we need to use call deferred for any action with the scene tree, say like adding a child?

navett52 | 2020-04-11 14:17

Yes, to quote the docs:

Interacting with the active scene tree is NOT thread safe. Make sure
to use mutexes when sending data between threads. If you want to call
functions from a thread, the call_deferred function may be used:

# Unsafe: 
node.add_child(child_node)
# Safe: 
node.call_deferred("add_child", child_node)

Though keep in mind, that we’re talking about doing this from a different thread, so when interacting with the scene tree from the main thread, you can just use the regular function calls.

omggomb | 2020-04-12 12:36