Task Library - Now Available!

Thank you that works perfectly! Although my micro-optimisation brain tells me there should be a way to disconnect or stop the task.delay because a lot of them could build up. However this solution is good, wouldn’t run into any issues with loads of task.delay functions being called anyway. Thanks ;D

1 Like

You normally do this with a cancellation token:

local doTheThingNumber = 0
function DoTheThing()
    doTheThingNumber += 1
    local thisDoTheThingNumber = doTheThingNumber
    task.delay(0.5, function()
        if thisDoTheThingNumber == doTheThingNumber then
            -- Do the thing
        end
    end)
end

You can even wrap the whole pattern in a higher order function (function that returns another function) if you’re going to be using it a lot:

function SingleDelayedFunc(delay, f)
    local token = 0
    local function handler(myToken, ...)
        if myToken == token then
            f(...)
        end
    end
    return function(...)
        token += 1
        task.delay(delay, handler, token, ...)
    end
end

local doTheThing = SingleDelayedFunc(0.5, function(arg1, arg2)
    print(arg1, arg2)
end)
doTheThing(1, 3)
doTheThing(4, 5)
4 Likes

There’s still the problem that the delayed function is eventually going to be called no matter what. Long-running “cancelled” delays will still be sitting uselessly in the scheduler. Additionally, if you pass a thread, you can’t safely reuse it until the delay is finished, or it will be resumed unexpectedly.

It would be great if delay returned a function or something that actually cancelled the delayed call.

4 Likes

I would argue that if you’re canceling things frequently enough that the extra threads sitting in the scheduler is a problem, then there’s other architectural problems going on. Cancelation should not be a hot path in your code.

4 Likes

Thank you for this! Exactly what I needed and very insightful. My code is eventually called so it isn’t an issue for me anyway. Thank you for the wrapper too, appreciate it.

Do you think task.delay would be the best method to go about this? I’m not certain how the task scheduler works and if this affects it much. Essentially every time I press F, this function would execute and call task.delay, so you could be spamming task.delay. That wouldn’t matter, would it?

If your user can press F fast enough to flood the thread scheduler in that way they’ll probably break their F key before they break your game :joy:

And if we’re talking server side, there’s probably way more expensive Remotes for an exploiter to spam than that one, if you’re trying to fully cover that avenue you really need handcrafted flood checks on every Remote that could do something too expensive and freeze the server.

4 Likes

This is honestly extremely useful, wait() has been my enemy for many years, and to finally see it improved with a custom way to handle the task scheduler is so great.

Since you said task.spawn could be expensive, would it be better to use a fastSpawn method or switch?

Also, I might just be dumb here, but do the task functions return RBXSignals? Or do we have to use another method to disconnect functions like delay (as someone mentioned above)

1 Like

LOL, that’s hilarious. Yea that makes sense, I always forget how fast code can run. Thank you for your help, I’ve learnt loads!

1 Like

task.delay has some problems with threads.

local thread = coroutine.running()
task.delay(1, thread, "foo")
print("RESULT", coroutine.yield()) -- "foo" expected
--> RESULT function: 0xa655f205db803cfd
local thread = coroutine.create(function(...)
	print("ONE", ...)
	print("TWO", coroutine.yield())
	print("THREE", coroutine.yield())
	print("FOUR", coroutine.yield())
end)
task.delay(1, thread, "one")
task.delay(1, thread, "two")
task.delay(1, thread, "three")
task.delay(1, thread, "four")
--> (error) attempt to call a string value
--> (error) cannot resume non-suspended coroutine
--> (error) cannot resume non-suspended coroutine
--> (error) cannot resume non-suspended coroutine
local thread = coroutine.create(function(...)
	print("ONE", ...)
	print("TWO", coroutine.yield())
	print("THREE", coroutine.yield())
	print("FOUR", coroutine.yield())
end)
task.spawn(thread, "one")
task.delay(1, thread, "two")
task.delay(1, thread, "three")
task.delay(1, thread, "four")
--> ONE one
--> TWO four
--> THREE function: 0xab050a169562a24c
--> FOUR function: 0xab050a169562a24c

Seems to occur with task.defer as well. Other functions may or may not also be affected.

3 Likes

When using the task library you should avoid scheduling a thread that has already been scheduled for resumption. This is due to how the methods are implemented internally. I’ll see if we can update the messaging around them to make this more explicit. If you do want to schedule the same thread multiple times you should definitely submit a feature request for it.

3 Likes

That’s gonna be amazing, can’t wait to use it in my game

So if I’m correct, we now have actual multi-threaded spawn and delay functions?
I needed that so much.

Uh no? They are still in serialized thread. They have still the behaviour of old spawn and delay functions minus the throttling part and they use Heartbeat internally instead.

1 Like

Is RunService.PreAnimation going to be the Missing event between Inbound Replication and Humanoids & Animations?

I just got around to trying to convert my code from FastSpawn to task.spawn

As it stands, the typings of task.spawn makes is near-unusuable without ugly-looking code. Why?

Well… currently the following code yields two script analysis warnings:

--!strict
task.spawn(function()
	print('Hello, World!')
end)

This typing is bad because it assumes that the argument you pass into task.spawn has to return something. So at very least the any being returned should be optional.

The following monkey code with modified typing works:

--!strict
local task_spawn = task.spawn :: (((any) -> any?) | thread) -> any

task_spawn(function()
	print('Hello, World!')
end)

That’s one problem solved—however, the argument(s) passed into task.spawn are also not variadic or optional. So you always have to pass a second argument to the task.spawn… even if you don’t want to

--!strict
task.spawn(function()
	print('Hello, World!')
end :: any)

You could pass nil here, however this does affect the tuple size.

This is currently the way I am using task.spawn, and it’s super ugly:

--!strict

(task.spawn :: any)(function()
	print('Hello, World!')
end)

HOWEVER, if I have anything before that line, I need to prepend the spawn line, or append the previous line, with a semicolon to avoid a syntax error:

--!strict
print("Hello?")
;(task.spawn :: any)(function()
	print('Hello, World!')
end)
--!strict
print("Hello?");
(task.spawn :: any)(function()
	print('Hello, World!')
end)

Please see if you can fix these typings. I’m stuck with some really ugly looking code.

4 Likes

task.wait() in a while loop never stops if the script is disabled or destroyed. This is similar to custom wait modules that use coroutine.yield. So I assume task.wait has a similar implementation but internally.

Reproduction:

while true do
	print("Running")
	task.wait(0.5)
end

Put this in a script, then disable the script. It will continue to print.
Now change task.wait to wait, then disable the script. It will stop.

With custom wait modules there was no way other than adding checks within the loop to fix this, but since this implementation is internal there should be a way to fix this. I hope it is fixed soon, it is the only thing preventing me from using task.wait and I cannot stand the original wait function much longer. So please fix this.

2 Likes

Oh i didn’t known.

But I don’t think inventing a new function just for different debugging is good and I think we should rather port the behavior of task.spawn to coroutine.wrap

I understand task.wait() vs wait() but is there a difference between task.wait(20) vs wait(20)? Should wait(x) be avoided at all costs now?

wait(x) bad
task.wait(x) good

Yeah basically task.wait(x) is a better wait. Though you should still remember to use event based coding when you can.

But you should replace spawn() with coroutine.wrap and delay with task.delay, wait with task.wait etc.

2 Likes