Godot Version
4.6.3
Question
This is driving me crazy. I have a simple messaging queue to handle some multiplayer logic, which obviously can just run in the background as fast as its thread will let it, but if I update it in _process or even with timers, it is limited by physics processing because Timer objects are internally tied to _process. I tested it, and processing 200 messages in a row takes around 0.2 seconds, whereas processing them one at a time via _process takes more than 6 seconds.
I want something simple like “process this every 0.001 seconds or when idle” instead of Godot’s “every 1/60th of a second or when idle”, but I cannot find any way to do this. Should I port the entire script to C++ just to gain access to a proper timer? That seems like an overreaction.
Edit: Just to confirm, disabling V-Sync solves this problem (by uncapping how often _process can be called), but that is obviously a stupid solution since it messes with everything else. Clearly _process is just not the right place to update non-display-related things. But where else to do it?
What’s in those 200 messages?
Do you want a thread? If you don’t explicitly create a Thread object Godot will operate (almost) entirely on one thread, it will only function in process time. 200 messages sounds like a lot for a single frame. Maybe you can share some code or more about what you are trying to do.
By deafult _physics_process is called 60 times a second, but you can configure it by changing Engine.physics_ticks_per_second
delta is the logical time between physics ticks in seconds and is equal to Engine.time_scale / Engine.physics_ticks_per_second.
But if you do that you probably want to tweak some things in the rest of your physics code to compensate, and that might be more of a headache, IDK.
I’m guessing you don’t expect the game to get 200 messages in .2 second at all times, and that this was just a stress test?
Otherwise I’d second @gertkeno this seems a little excessive
Make a background thread for this. Wait for the incoming messages and process them. Any kind of timer is not a good solution.
This happens essentially once, namely at startup when a bunch of agreements needs to be reached between clients in a sort of round robin implementation, so the bulk of these messages are simply acks to ensure consensus. Otherwise I expect something like at most two messages a second, but I still don’t want the startup time artificially inflated.
I tried making a standard worker thread for this, but whenever I use an infinite loop without a timer in it, Godot simply freezes. I assumed that is some sort of engine limitation.
var processing_active: bool = true
func process_message_queue() -> void:
while <this worker thread should process>:
# await get_tree.create_timer(0.001).timeout
if processing_active and queue.size():
processing_active = false
queue_mutex.lock()
var a: Action = queue.pop_back()
queue_mutex.unlock()
await a.do_stuff()
processing_active = true
This freezes the game when the queue is empty unless the timer is put in, even when I put it in a separate thread. So I am not sure how to “wait for messages” in GDScript.
Each a.do_stuff() completes in around 0.2 milliseconds, so waiting for the next frame is incredibly wasteful.
Edit: As a test, I increased both “physics ticks per second” and “max physics steps per frame” to 500 and put the queue processing into _physics_process. For some reason this is very noticeably slower than putting it in _process without VSync.
Try something like this:
func loop_stuff() -> void:
while should_still_process:
await get_tree().create_timer(0.001).timeout
while got_something_to_process()
process_something()
Process messages in a busy loop as long as there are messages to process. When the queue is empty, sleep for a while and try again.
If you make a infinite loop in a main thread (or any thread), the thread cannot do anything else than run the loop. That’s how all programs work, not just Godot.
Yes, that is exactly what I have right now. Like I said, the problem with this is that timers can only timeout on a frame, so this actually processes every 1/60th of a second, not every millisecond. Which causes unnecessary delay.
OS.delay_usec() or OS.delay_msec() instead of await get_tree().create_timer(0.001).timeout might help, but then again, that would be a busy loop. But a 0.2 s busy loop when a game starts is not the end of the world.
Thank you, that is exactly what I was looking for! Neither “timer” nor “sleep” turned up any results in the documentation search. Solved!
Why would you need a delay if processing is done in a thread?
I don’t know, I just accepted that this is apparently a thing. If I take out the delay in the worker thread, then the main thread locks up. Godot does a lot of things that I just accept as quirks.
You should not await in a thread.
To clarify this: await has the code continue on the same thread as the signal is emitted on, which is usually the main thread, which is unsafe generally (and also risks blocking the main thread if the assumption is that the thread is a separate thread)
It’s still pretty unclear what the exact problem is you are facing, if this is a thread and it’s processing too much (i.e. spin-lock) it could lag the rest of your computer given a low-thread count. If it’s not a thread or you are using .wait_to_finish() on it much too soon then it’s going to lock your main thread. processing_active will always be true when the if statement is run, I’d bet the await isn’t really waiting on the function one way or another. You could also be experiencing a deadlock with the mutex. In total more code would help diagnose the problem.
Seems like you should use a semaphore or mutex to control when this executes, this lowers the time spent processing significantly.
func background_thread() -> void:
while not_exiting:
semaphore.wait()
queue_mutex.lock()
while queue.size() > 0:
var action := queue.pop_back()
action.do_stuff()
queue_mutex.unlock()
func add_action_queue(my_new_action: Action) -> void:
queue_mutex.lock()
queue.push_front(my_new_action)
queue_mutex.unlock()
semaphore.post()
Today I learnt that the await keyword is not thread safe.