Task Library - Now Available!

From my understanding task.wait() is more like RunService.Heartbeat:Wait() instead of wait().

:Wait() is different from wait() because it’s waiting for the event tied to it to fire. wait() however may take longer to wait than it needs to which is why it should be avoided.

I don’t think it matters if you use RunService.Heartbeat:Wait() or task.wait(). They both do the same thing (from what is told). Personally I would only use RunService.Heartbeat:Wait() if I was using RunService for other things in my code (asynchronous threads and everything) and I specifically needed to wait for the Heartbeat event. I would only do this for code readability. Otherwise I would use task.wait()

2 Likes

We need to resume delayed threads at some point in the engine step and Heartbeat was the logical choice for this as it runs after all other internal engine steps. However, this is an implementation detail. The primary reason to use wait is to yield a script for some amount of time rather than to bind code to the heartbeat step. If you want to bind code to the step then you should definitely be using:

RunService.Heartbeat:Connect(function (stepTime)
  -- do something
end)

This is why wait returns the elapsed time rather than the step time.

Heartbeat fires once every frame. If the engine is running at 60 FPS then the event will fire 60 times a second. If it runs at 40 FPS then the event will fire 40 times a second and so on. Passing a duration of 1/60 does not guarantee the thread will be resumed in one 60th of a second, just that at least one 60th of a second will have passed before the thread is resumed.

2 Likes

It depends what you want. In my estimation, thanks to being throttling-free and the other changes task.wait() should be good enough for almost all use cases.

If you really want to fine tune your game there may still be room for custom scheduling solution built in Lua that does let you throttle execution but in a way that you define, however, almost no-one should need to go to those lengths.

6 Likes

That’s naturally expected given the way the API works.

task.wait() in a loop will always yield 1 frame as an edge case, but you’ll never be able to yield precisely n frames with task.wait(n/60) regardless of how it’s implemented, because not every frame is exactly the same length and you will run into aliasing issues. The only sensible way to do that would really be to have a separate API something like task.waitframes(n) which doesn’t care about wall-clock or in-engine time at all, only frame count.

You could also do this yourself as:

local function waitNFrames(n)
    for i = 1, n do
        task.wait()
    end
end
6 Likes

Sweet, then it’s good to see that it runs at a C closure.

Looks like it’s more performant than thread handlers, I presume!

I read task library, I expected something like… a task manager… for projects in studio. Nope. Still a good feature I for scripters!

coroutine.wrap starts a coroutine under that script. This is different in that it doesn’t create a new context, and if the script is destroyed, the coroutine also automatically dies.

task.spawn and task.defer however create a new thread underneath Roblox’s task handler, which is it’s own script context and wont stop if the calling script is destroyed.

Since you’re passing in a callback (or thread) from the calling script, you still get access to that script’s environment, but internally, the thread is running under a different system.

2 Likes

Thank you for this, this is exaclty what I was looking for ontop of multithreading.

Definitely excited for this, will make coding much more organized for me.

I really love task.spawn making guns now is gonna be a lot better and the players wont feel as much lag

Now people can’t complain that tutorials are deprecated since all tutorials are now deprecated… :joy:

But in all seriousness, very nice to see improvements in these parts. :eyes:

1 Like

Finally, a new way to apprehend such difficulties. With that being said, Roblox adding this new method gives us developers a bigger reputation in problem solving and development in general. Not only is the method itself is suffisticated but it also holds an important role in Programming!

I’m pretty annoyed that the entire post is significantly more than this, but you only clipped that section out.

The task API is great and all, but task.wait(), with no arguments, still has the same code smell as wait(), because it was never about the internals.

6 Likes

Do you mind explaining what “throttling” means?

2 Likes

Only 10% off a frame’s time will be used resuming things which were spawned / which waited. If resuming of those waiting things takes longer than that 10%, the ones in excess of the 10% time budget will be delayed to a future frame even though their timeout has already elapsed. This can be problematic, because by the very end of the frame there may actually be a significant amount of time left over which could have been used resuming more threads on more powerful devices.

task.wait() doesn’t have this throttling, everything you task.wait() will resume exactly on the frame the wait time is up regardless of the consequences (potential lag), which gives you more control.

8 Likes

This new library is a great replacement of wait and other functions, but I experienced a huge problem with it. If you delete a script that has a loop which utilizes task.wait, that loop will continue to run for an infinite amount of time unless it has special additional checks in place to stop itself. Using .RenderStepped:Wait() or standard wait would stop the loop and thus disabled script wont actually run anymore.

While this makes sense from standpoint of how this new library works, its counterintuitive for people who are used to behavior of standard wait, and if not handled properly it can make stuff lag over time (especially if that loop is located in an object that gets destroyed/respawned a lot, like character or vehicles with scripts in them)

3 Likes

Coming back to this post but… I don’t quite understand the use case of task.defer. Anyone able to explain when it would be appropriate to use? I’m especially keen on this because I want to get started on working with Deferred signals so I’m future-proofed when Immediate signals go poof.

I’ve got the hang of most of the other events but deferred stuff confuses me heavily even after checking out the diagram and everything from the other thread. I’m not sure when and where I should use it or what kinds of problems it’d help me solve. Is it stupid to defer everything? When does code not in task.defer run? Just in general I don’t really understand deferring stuff.

EDIT: I did find an interesting use of task.defer. It doesn’t help me understand what exactly deferring is or what task.defer is used for, but I tested it out a bit and made a script that prevents a tool from being unequipped. It works in both Immediate and Deferred signal modes.

local Tool = script.Parent

local Character

Tool.Equipped:Connect(function ()
	Character = Tool.Parent
end)

Tool.Unequipped:Connect(function ()
	if Character then
		task.defer(function ()
			Tool.Parent = Character
		end)
	end
end)
2 Likes

See above: Task Library - Now Available! - #38 by tnavarts

2 Likes

The defer method schedules code for resumption at the end of the current (or next if we aren’t currently in one) resumption point. You can think of it as adding something to the back of the queue of threads we are going to resume.

So it’s useful in cases such as the example given by @tnavarts and the one you’ve shared here but it can also be useful for other purposes too.

  • Scheduling custom events to run later so they don’t affect the current code (we do this with deferred events)
  • Coalescing state changes so code only runs once

An example of the latter might be a case where you only care about the final value of something and not any intermediary values:

local currentRequestTime = 0

local function performDataSave(requestTime)
    if requestTime < currentRequestTime then
        return -- a newer data-save was requested
    end
    -- save data
end

local function queueDataSave()
    currentRequestTime = os.clock()
    task.defer(performDataSave, currentRequestTime)
end

instance:GetAttributeChangedSignal("Points"):Connect(function ()
    queueDataSave()
end)

Of course there are other ways to implement this but this is one such way to do it.

5 Likes

Hey, sorry if I’m missing something in terms of engine implementation detail, but what difference is there in simply calling the Object::Destroy method directly and using task.defer? What engine detail is changed? Going off the comment of it being destroyed instantly, does calling the method directly have a form of delay before the object is destroyed?
Many thanks beforehand.

1 Like