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
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)
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.
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.
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
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.
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)
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!
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.
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.
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
delay functions minus the throttling part and they use
Heartbeat internally instead.
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.
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.
while true do print("Running") task.wait(0.5) end
Put this in a script, then disable the script. It will continue to print.
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.
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
wait() but is there a difference between
wait(x) be avoided at all costs now?
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.