Task Library - Now Available!

It’s not a “bug” in that coroutine.wrap / coroutine.resume propagate errors in the same way that they do in vanilla Lua, they just don’t have the extra behavior that Roblox adds for convenient debugging in Studio. It makes sense to have a separate library like this if you want the Roblox behaviors so that vanilla Lua code can be brought into the engine from other sources and work as expected.

3 Likes

I see no need for this at all
task.wait() is fine as it is, besides if wait() is deprecated then it WON’T be updated anymore, it is still going to be useable so older games don’t break

I don’t think it’s all evil in all honestly. I use spawn() mainly for its task scheduler functionality which helps performance a lot in looped events such as character leg movement replication, bullet replication and stuff which doesn’t need to be ran instantly

@WallsAreForClimbing and @tnavarts, looking at the spec provide for task.wait() and docs for wait() you can see they do not return the same number of parameters. wait() returns the duration and the total game time, whereas task.wait() is only returning the duration. Devs should be aware of this difference when updating their code.

Is there any reason to not make task.wait return both parameters?

Below is the last 4 lines of the current Character.Animate script for reference. You will get unexpected behaviour assuming they are equivalent and just replace wait() with task.wait().

while Character.Parent ~= nil do
    local _, currentGameTime = wait(0.1)
    stepAnimate(currentGameTime)
end

changing to task.wait(0.1) will not give you the expected results:

while Character.Parent ~= nil do
    local _, currentGameTime = task.wait(0.1)
    stepAnimate(currentGameTime)
end
3 Likes

Does this mean that task.wait() has a minimum time of 1/60 instead of wait()'s minimum time of 1/30?

1 Like

Much less than 1/60 in fact!

game:GetService("RunService").RenderStepped:Wait() 
print(task.wait())  --> 0.00097130000000334

If you task.wait() during the Heartbeat part of the frame, then it will wait until the next Heartbeat (roughly 1/60), but if you task.wait() in an earlier part of the frame (such as on RenderStepped), it won’t even wait a full frame, it will unsuspend on Heartbeat of the same frame.

For reference, a frame works like this:

image

16 Likes

Are you sure about this? If you create a LocalScript, do task.spawn(function() while wait(1) print(1) end), and then delete it, it will stop printing.

2 Likes

Very much yes! Is this a solution to deferred events? Will this allow Parallel Luau to exist without breaking all event-handling code across all existing games?

Are you able to disconnect task.delay? I want to disconnect task.delay if a certain condition is met so it doesn’t run and I’m having trouble doing that.

Can’t you just add an if statement inside the function passed to task.delay and check if a certain condition met like that?

This is what I’m trying to do basically. Say you press the F key, it will turn the part red, then after 0.5 seconds, it will turn the part green. However, if you press the F key while the part is red, then I want it to disconnect the current task delay, and add another 0.5 seconds of wait time before the part turns green. I can’t think of another solution, I’m probably missing something obvious, any help would be appreciated.

    local conn
    function TurnPartRed()
        if conn ~= nil then
            conn:Disconnect()
            conn = nil
        end

        Part.Color = Color3.fromRGB(255,0,0)
        conn = task.delay(0.5, function()
            Part.Color = Color3.fromRGB(0,255,0)
        end)
    end

You can add a variable that stores the last time F key was pressed then check if current time subtracted by the latest F key input time is bigger equal than 0.5:

    local conn
    local LastFKeyPressTime = 0
    function TurnPartRed()
        if conn ~= nil then
            conn:Disconnect()
            conn = nil
        end
        
        LastFKeyPressTime = os.clock()
        Part.Color = Color3.fromRGB(255,0,0)
        conn = task.delay(0.5, function()
            if os.clock() - LastFKeyPressTime >= 0.5 then
                Part.Color = Color3.fromRGB(0,255,0)
            end
        end)
    end
3 Likes

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)
7 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.

5 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.

6 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