Task.defer voiding varargs

Behavior #1

local function foo(n)
    print(n)
end
for i = 1, 5 do
    task.defer(foo, i)
end
warn("eof")

The above code prints the following:

  15:46:35.564  eof  -  Edit
  15:46:35.570  1  -  Edit
  15:46:35.570  2  -  Edit
  15:46:35.570  3  -  Edit
  15:46:35.570  4  -  Edit
  15:46:35.570  5  -  Edit

which is to be expected, it’s deferring in the order they’ve been given.

But when I wanted to recycle the thread to avoid creating a new one every time I defer, I ran into this weird behavior:

local count = 0
local co = coroutine.create(function(...)
    while true do
        count += 1
        print(`{count}: {coroutine.yield()}`)
    end
end)
coroutine.resume(co)

for i = 1, 5 do
    task.defer(co, i)
end
warn("eof")

Which prints:

  15:55:02.542  eof  -  Edit
  15:55:02.549  1: 5  -  Edit
  15:55:02.550  2: nil  -  Edit
  15:55:02.550  3: nil  -  Edit
  15:55:02.550  4: nil  -  Edit
  15:55:02.550  5: nil  -  Edit

So now when we pass a suspended thread, it manages to run it the n amount of times in the correct order but only the last defer’s arguments are kept and all the others get voided which is very weird behavior.


Behavior #2

And here’s another voodoo magic behavior:

local count = 0
local co = coroutine.create(function(...)
    while true do
        task.wait()
        count += 1
        print(`{count}: {coroutine.yield()}`)
    end
end)
coroutine.resume(co)

for i = 1, 5 do
    task.defer(co, i)
end
warn("eof")

By adding a task.wait(), the prints somehow becomes:

  16:17:53.829  eof  -  Edit
  16:17:53.841  1: nil  -  Edit
  16:17:53.842  2: nil  -  Edit
  16:17:53.842  3: 0.013875399999960791  -  Edit
  16:17:53.843  4: 0.0008567999993829289  -  Edit
  16:17:53.859  5: 0.01600220000000263  -  Edit

The reason I didnt do an Expected Behavior section was because I dont really know what the engineers say the expected behavior should be. So this is just a pure bug report.

8 Likes

A single Luau thread (aka coroutine) can only be scheduled once at a time in the task scheduler.

We have already prepared a warning (and an error in the future) for this case and will be enabling it soon.
‘Behaviour #1’ will print:

And similarly, ‘Behavior #2’:

5 Likes

In 'Behavior #'1, the Luau thread was able to run 5 times and do work as expected even though it was deferred previously, which tells me the task scheduler is capable of deferring deferred threads but loses track of the varargs. Wouldn’t it be more advantageous to actually fix that instead of doing a guard clause? Because it’ll make it similar to task.spawn in that behavior which would make the eventual transition to defer being the default better?

Of course I know nothing about how doable that fix would be but…

3 Likes

‘Losing arguments’ and ‘Replacing arguments with magic garbage’ is not what I would call ‘expected behavior’.
In the case it’s used without arguments it’s just a footgun until the moment arguments are added for one reason or another.

Because the system doesn’t work in the current form with multiple schedules, we decided that the best way is to block it from being used incorrectly (especially unintentionally).

Maybe in the future we can actually support scheduling multiple times, but this is very rarely requested.

3 Likes

Why doesn’t it just properly resolve the arguments and cancel the previous task.defer instead of overwriting the varargs but still deferring it?

We should atleast have a way to find out if a thread is currently already “deferred” or “waiting” in the task scheduler, that way we can properly avoid this future error instead of having to pcall(task.defer, thread) to make sure it never occurs.

1 Like

Let it be known that the demand is two, rather than one:

3 Likes

While it may be rarely requested, it most certainly could be made into a module that’s used often.

Number #1 candidate for this would be a task.defer implementation of GoodSignal as it uses thread recycling to avoid thread creation which is considerably slower, but it uses task.spawn, and when I attempted to make it use task.defer, which is what roblox recommends, I ran into that varargs issue which as far as I can think killed any hopes of recycling the thread.

2 Likes

You should now be able to see warnings in the Output telling about threads being scheduled multiple times in the task library.

2 Likes