Coroutines hangs on tween.finished if tween.kill() is called

Godot Version

4.5.1stable

Question

This is perhaps me not understanding await or a bug, related to Add `become_invalid` signal to Tween · Issue #13296 · godotengine/godot-proposals · GitHub

In the original issue, someone mentioned that hanging on tween getting killed is not a problem, but that’s not always the case.

If a function B awaits another function A which await tween.finished but tween was killed, B will hang.

normal case A.

normal case B, tween killed but fine, _suspended_1() gets cleared away.

problem. _suspended_2() hangs and keeps piling up in memory.

I’m now not sure if this is expected behavior or a bug.

extends Node2D

var time :=0.0

var idx:=0
var tween:Tween

func _process(delta: float) -> void:
	_suspended_2()

func _suspended_2()->void:
	await _suspended_1()

func _suspended_1()->void:
	if tween:
		tween.kill()
	tween = create_tween()
	tween.tween_property(self,"time",time+1,1.0)
	await tween.finished
	print("finished tween %d"%idx)
	idx+=1

Connect to the signal instead, this won’t create a coroutine and will the connection will be cleaned as the signal is killed.

Could you avoid creating coroutines and tween in _process? That can cause many issues on it’s own.

it’s just for showcasing, The behavior holds no matter where you create it.

What is “hang”?

You create a fresh tween and a sleeping coroutine every frame. Piling up is expected.

It’s suppposed to pile up to a few hundred objects as shown in normal case A then get cleared away. In problem case, the coroutines created stays forever.

A sleeping coroutine stays forever if no one wakes it up. That’s how it’s supposed to work.

That’s what I thought initially, but quote ydeltastar from the linked issue,

me:However, if you have await tw.finished somewhere in your code, that coroutine will be dangling forever if tw is killed.

they:You don't need to worry about this. await tween.finished is basically implemented as tween.finished.connect(function_state.resume, CONNECT_ONE_SHOT). When you assign a new tween, the previous is freed, so all signal connections are cleared, which also frees all pending callbacks and function states (coroutines).

Which I tested out to be true in case B. So what is the problem here? why is the coroutines still piling up in case C?

The coroutine _suspended_2() awaits for coroutine _suspended_1() to finish, which will never happen. When the tween is killed the function state object for coroutine _suspended_1() may be deleted but the coroutine will still never continue because the signal won’t be emitted. So _suspended_1() will indefinitely be stopped at await. Since it never finishes it will keep the _suspended_2() state object allocated forever.

1 Like

I see, thanks for the explanation, I was assuming freed coroutines will unblock other coroutines in a chained fashion.