What is better use for delaying? task.delay() or coroutines?

A question has been bothering me in my mind, It’s about either task.delay() or coroutines is better for delaying functions/threads/code

Is this better?

task.delay(2, function()
	print("delayed")
end)

or this?

coroutine.resume(coroutine.create(function()
	task.wait(2)
	print("delayed")
end))

My short answer is neither. They’re not functionally different and they both achieve the same goal in very similar ways.


There really is no functional difference between these two. The task library provides a simpler way to create and schedule new threads, and one is not better than the other. It entirely depends on your use case.

The coroutine library simply provides a way to create and control threads.

These two pieces of code behave identically:

task.spawn(function()
	task.wait(2)
	print("delayed")
end)
-- is identical to
coroutine.resume(coroutine.create(function()
	task.wait(2)
	print("delayed")
end))

Meanwhile, task.delay is not spawning the thread immediately at all. Instead, the thread is spawned after the delay period (which, is definitely a lot different).

This might matter if you want to run code before you start your delay but in a new thread, such as if you want to keep track of this thread and then later task.cancel it:

local childThread
task.spawn(function() -- Spawn this function in a new thread immediately (just like the coroutine.resume + coroutine.create combo)
	childThread = coroutine.running() -- Grab the running thread
	task.wait(2) -- wait 2 seconds (now the code after task.spawn will run)
	print("delayed")
end)

-- Cancel the thread immediately (now it won't run)
-- If you comment this out, you'll see "ran", then "delayed" after 2 seconds
task.cancel(childThread)

print("ran")

Another way to write this (with coroutine would be like so):

-- Create the child thread
local childThread = coroutine.create(function()
	task.wait(2) -- wait 2 seconds (now the code after task.spawn will run)
	print("delayed")
end)
-- Start the child thread immediately
coroutine.resume(childThread)

-- Cancel the thread immediately (now it won't run)
-- If you comment this out, you'll see "ran", then "delayed" after 2 seconds
coroutine.close(childThread)

print("ran")

You can even combine the two libraries if you felt like it:

local thread = coroutine.create(callback)

-- Defer starting the thread
task.defer(thread)

P.s. @Valkyrop the replies in the post you linked are unfortunately misinformative imo. You can start/stop task.spawned threads, you can really do that with any thread, it’s just a feature of lua threads.

coroutine is nothing more than a library to work with lua threads in lua. Meanwhile, despite being very similar, the task library acts as a versatile way to schedule threads. Both ultimately are two ways to interact with threads, and coroutine and task both see their own unique uses that the other doesn’t naturally support (except technically task.spawn but imo task.spawn(callback) is a lot nicer to write than coroutine.resume(coroutine.create(callback))).

If you couldn’t start/stop a thread created by task.spawn like you can in coroutines, you couldn’t call task.wait in it, because internally, all task.wait is doing is yielding and then resuming the thread you call it in after the time you specify has elapsed.

14 Likes

Heya, I came across this searching for which method would be faster. I am not familiar with microbenchmarking though could imagine you are.

Would you know which is more performant, task or coroutine?

Roblox created the task. library to solve a lot of issues with simple coroutines. Generally task. will be faster because it uses Roblox’s specialized scheduler instead of lua’s generic version.

A late response, but if you want a cleaner and modular way to delay threads, I suggest using evaera’s Promise module. The module has multiple handy asynchronous functions and is based on JavaScript’s promises. Moreover, it is widely used in the top Roblox games. Here is how you would use it for your use case:

Promise.delay(5)
    :andThenCall(print, "This runs after 5 seconds")
    :catch(error)

It utilizes method chaining, which makes it more pleasant to look at and, of course, manage.

Unless you are constantly spawning thousands of threads at once (probably not a good idea), there is not a big enough difference for it to ever matter.


I know that probably kinda sounds unhelpful, but, generally you care about either a significant performance difference for single calls (which there isn’t one here) or a small performance difference where you are making a lot of calls (usually I start paying attention past ~1000 calls, or if it is generally expensive enough to noticeably slow things down for a smaller number of calls) since that small difference will compound to create a significant amount.

Being a little bit wasteful isn’t a bad thing when it comes to performance if it doesn’t actually affect anything.

With the new script profiler section in the dev console (F9 key) you can take quick snapshots of the different scripts in the game. It isn’t as precise (or as consistent) as the microprofiler since the microprofiler gives you hard numbers and stats, but it is great if your game is laggy and you want to figure out why. You can see how much time one thing takes relative to other things, and that is quite handy when you know something is running slow and just don’t know what it is.

I would suggest using it since it is very accessible. If you want to find out more specific info, the Microprofiler may seem pretty confusing but ultimately you generally won’t care too much about most of its functionality except in specific circumstances. Usually I switch it to the Detailed view and then I can take a look at the timing of different things. With web dumps, you can also open up the html dump and hover over a bar and hit the Tab key to have it navigate to the most expensive one of those bars, which can be helpful if you want to identify lag spikes for example.


Some helpful numbers, for the game to maintain 60 fps (or 60 heartbeat) all of the client (or server for server heartbeat) stuff needs to add up to < ~16 ms (1000/60) for the frame. Generally I like to try to “reserve” about 8 ms from that total to save some room for rendering for clients, leaving the other 8 ms for all my game’s code. I try to keep that lower if I can, if everything in the game is taking near 8 ms, I know that low end devices will probably start struggling more since some devices can take a bit. If you want to be even safer, try to keep your game’s client stuff under 6 ms for example.

There are cases where you won’t be able to avoid some spikes sometimes which is okay. For example, loading or removing big models is, relatively speaking, a lot of work, and you’re not really going to be able to avoid the cost from that because of that. Generally clients aren’t going to take notice too much there.


Tl;dr

You should use whichever one is most natural and makes the most sense for you. Spawning a thread has a tiny cost, but the method used to spawn the threads won’t effect how performantly the thread runs or anything.

1 Like