2
\$\begingroup\$

Consider a situation where a coroutine in node A awaits a signal from node B. If node B gets freed without sending the signal, obviously the coroutine in node A will never finish executing. Does it still take up memory, waiting for something that will never happen? Or is the engine smart enough to free up the stack frame/execution context/whatever when the signal it's waiting on goes away? Does it make a difference if the thing that node A is waiting on is a coroutine in node B rather than a signal?

\$\endgroup\$
1
  • 1
    \$\begingroup\$ What do you observe when you try to replicate this behaviour? \$\endgroup\$ Commented Mar 20 at 10:22

1 Answer 1

2
\$\begingroup\$

I do not know if it being a coroutine makes a difference. But I can tell you that await has no means of being cancelled, and yes it can be there forever and leak memory.

Using await creates a GDScriptFunctionState (this used to be exposed in Godot 3, but it is not in Godot 4). The GDScriptFunctionState internally holds a reference to a CallState which Godot uses to resume execution (it has the stack et.al.), and it could clear the CallState to avoid these leaks, but currently does not. There is a fix being worked but it didn't make it for Godot 4.4.

By the way, your scenario is not the worse: if the side awaiting is freed, and the other part signals expect a runtime error when Godot tries to resume execution on an object that is no longer there.


When I first migrated away from await, I decided to refactor to signals instead. Godot will disconnect the signal connections automatically when either party is freed, so it causes no problems. Just to be clear, await on signal is not a signal connection.

You can use the CONNECT_ONE_SHOT to tell Godot to disconnect the signal automatically when it is received (or disconnect it on the continuation, which is usually not much trouble anyway). And, by the way, remember that you can take advantage of anonymous "lambda" methods.

Refactoring your await code to use continuations might get messy depending on what you are doing.

Refactoring a loop from await to signals is similar to refactoring a loop to recursion. First, if you are using await in the middle of a loop, you might have to do a partial unroll so await happens at the start of the iteration, afterwards it is easy to make the body of the loop into a continuation. Then you will find that it is a good idea to have a method make a Callable to itself using bind to curry the arguments for the next run, and connecting to a signal.

Some issues you might run into:

  • If you bind a node and it becomes invalid, it will bind as a null and fail. Instead pass it inside of an array or dictionary, which is also convenient if you are passing multiple objects at once.

  • In the odd case you need to connect the same method of the same instance to a signal multiple times (which Godot does not allow), you might want to make a class to hold the arguments and dispatch the call, so you can connect different instances of the class to the same signal.

Worse case scenario you will make a state machine: have a value to indicate what to run next, and dispatch with a match statement. Although I ended up resolving the situations that I ran into where this was useful by different means.

\$\endgroup\$
2
  • \$\begingroup\$ You say " if the side awaiting is freed, and the other part signals expect a runtime error when Godot tries to resume execution on an object that is no longer there." Trying this out in Godot 4.3, I don't see any error. \$\endgroup\$ Commented Mar 21 at 16:02
  • \$\begingroup\$ @CarlMuckenhoupt It might be resolved. What I can find is that github.com/godotengine/godot/pull/81605 added checks for freed objects. However, I'm not sure if this covers all cases. \$\endgroup\$ Commented Mar 21 at 19:11

You must log in to answer this question.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.