Task.defer and task.delay have strange behavior when trying to resume a thread that is not suspended

Reproduction Steps

The functions task.defer and task.delay have strange behavior when the coroutine provided to them is not suspended, or if the coroutine is being scheduled for resumption while already being scheduled to be resumed later.

Expected Behavior

If the coroutine provided to task.defer or task.delay is dead, nothing should happen.
If the coroutine provided to task.defer or task.delay is running or normal, it should be resumed correctly when it would normally be resumed.
If a coroutine is being scheduled for resumption while already being scheduled to be resumed later, these should not interfere and both resumptions should happen correctly.

Actual Behavior

If the coroutine provided to task.defer or task.delay is dead, the provided arguments will still be put into the thread (which can resurrect the thread!):

local co = task.spawn(function()end)
print(coroutine.status(co)) -- prints "dead"
task.defer(co,print) -- puts print onto the coroutine
-- which makes it look like a freshly created coroutine
print(coroutine.status(co)) -- prints "suspended"
task.spawn(co,1,2,3) -- prints "1 2 3"
print(coroutine.status(co)) -- prints "dead"

If the coroutine provided to task.defer or task.delay is running or normal, the coroutine will be resumed but without any of the extra arguments provided:

task.defer(coroutine.running(),true)
print(coroutine.yield()) -- prints nothing

If a coroutine is being scheduled for resumption while already being scheduled, some strange stuff happens:

local co = coroutine.create(function(...)
	warn(...)
	while true do
		warn(coroutine.yield())
	end
end)
task.defer(co,print,1,2,3)
task.defer(co,print)
-- printed normally:
-- 1 2 3 function: 0xXXXXXXXXXXXXXXXX
-- error printed:
-- cannot resume dead coroutine

Workaround

These issues can be worked around by creating a new coroutine that will solely resume the original coroutine.
For example:

task.delay(1,task.spawn,coroutine.running(),true) -- will create a new coroutine that just resumes the main coroutine
print(coroutine.yield())
task.defer(task.spawn,task.spawn(function()end)) -- no error, just ignored
task.defer(task.spawn,task.spawn(function()end),true) -- errors, might be another bug
local co = coroutine.create(function(...)
	print(...)
	while true do
		print(coroutine.yield())
	end
end)
task.defer(task.spawn,co,1)
task.defer(task.spawn,co,2)

Issue Area: Engine
Issue Type: Other
Impact: Moderate
Frequency: Rarely

3 Likes

Thank you for the report.
We are aware about multiple issues with task.defer and task.delay from earlier reports on DevForum.
We are planning to make some changes in the future to solve or improve these functions and we’ll take into the consideration new cases around dead coroutines that you’ve reported here.

1 Like

As an update, the first two cases will now throw an error if a dead coroutine is re-scheduled to run with arguments.

There are remaining issues with task.defer being allowed on a dead coroutine (will not be called) and with ability to defer the same coroutine multiple times (will be called once with all arguments).

We have added warning messages for cases when threads are scheduled multiple times.

These are not errors yet, as we see that many experiences still hit these conditions (after 4 months) and we don’t want to break them with a hard error.

While this might not be what you had in your ‘expected behavior’, we consider the issues addressed and fixed.