Coroutines V.S. Spawn()... Which one should I use?

The title says it all… Basically I want to know in what scenarios I should use:

spawn(function()
    -- Script
end)

And what scenarios I should use:

local thing = coroutine.create(function()
    -- Script
end)

coroutine.resume(thing)

So… When is it best to use spawn(function() and when it is best to use coroutines?

26 Likes

Is that their only differences? Also how long is the wait when using spawn?

… How long… Is one tick?

See below.

Old response

They serve the same function. Spawn just has a built-in wait statement, where wait is the minimum time that can be passed to wait. Nothing about tick here, tick describes a passage in game time.

Canonically for pseudothreading you should be using coroutines. Spawn is a Roblox-specific global meant to execute something on demand. With coroutines, you can create one via wrapping (coroutine.wrap) or direct creation (coroutine.create) and execute it as in when you call it (coroutine.resume or calling a wrapped function), as noted right in the code sample in the OP.

There aren’t different scenarios where you use one over the other. Pick the one you find is easier to work with or fits your use case more appropriately. You can use one or the other or both of them or none of them in your codebase.

14 Likes

EDIT (2020-08-12): This post gets a lot of attention. If you want to learn more, you can read this brief article.


Please do not use spawn. It’s a hold-over from Roblox’s 30hz pipeline and has no guarantees about when (or if) it will begin executing the new thread. spawn is especially evil because it seems innocent at first, but once you have a lot of code that all uses spawn, you can see significant delays in execution upwards of several seconds in a resource-intensive game. Roblox throttles execution of threads and if the device is too bogged down, your thread could just get deferred forever.


This is bad advice. It’s easy to answer when you should use spawn: never. There is never a situation where you want to run some code “maybe at some point in the future or never”. Even for things that seem inconsequential, you can end up shooting yourself in the foot if things run in an order that you don’t expect them to.

Don’t set yourself up for failure. Use coroutine.wrap if you need to begin a new thread. Never use spawn, delay, or wait() in your code.

101 Likes

Can you provide some source of information for

because as far as I know this is not true as spawn function simply waits for the Task Scheduler and then creates a new thread. There indeed may be a delay (depending on the Task Scheduler queue) but it will definitely create a thread (you say that sometimes this will never happen and that’s what I’m questioning).

Spawn is simple and I don’t get why you say that you shouldn’t use it. I use it for example to run autosave function in my datastore manager.

Coroutines actually run instantly and this is an advantage. Also you have some coroutine functions to manipulate them which makes coroutines more useful but still spawn is not that bad in some cases.

9 Likes

In the Aquaman event game that evaera and I worked on in 2018, we saw spawn resumes taking as much as 15 seconds or more rather than the next couple of frames due to there being a budget for resuming threads. This is because we spawned a medium number of threads since we used in all Promise(-like) patterns in the game. Performance was not affected at all (game ran at 60FPS otherwise and other subsystem load (such as physics) did not affect the resume delays), the budget just seems very easy to exhaust when you’re doing anything of some complexity.

After switching from spawn to coroutine.wrap / firing a BindableEvent to start a new thread, and using a Heartbeat-based wait rather than wait(), all slowdown issues were resolved because all of the threads we spawned now ran within an expected time frame.

I would link you to engineers mentioning information on how this worked publicly, but unfortunately I think I’ve only talked to them about this in private channels, so I can’t link to any comments. I can assure you there is actually a resume budget and that problems like these can actually happen to you in a production game. It is guaranteed to resume at least 1 thread in the queue every resume cycle, but that’s about all the guarantee you get.

Save yourself some horrible nights of debugging and don’t use spawn / delay / wait. You want your threads to start predictably, the convenience of spawn is not worth it for serious projects.

39 Likes

Coroutines will eat up your stack info. Say goodbye to effective debugging. Use spawn if you can (except please consider @buildthomas’s point above) . If you need it to run instantly, use @Quenty’s trick to do a “fast” spawn by firing a RemoteEvent. I’ve been using this for mostly everything now for the sake of having accurate tracebacks.

22 Likes

What’s that? Never heard of it.

1 Like

I should have called it a stack trace instead. When an error is thrown, it will usually show the stack trace which walks back to where the error occurred and subsequent calls that lead up to the error. You’ll also hear it called a traceback.

When you see an error in Roblox, you will see it in red, and the traceback below it is in blue.

For various reasons, using coroutines really messes with this process and makes it really hard to do effective debugging in larger applications.

17 Likes

I’ve tested by making a lot of spawn and it does indeed becomes slower with a larger amounts of Threads, I didn’t expected coroutine.wrap, coroutine.resume to be superior

This is new info to a lot of us and frankly terrifying to hear such a horrible side effect of Built-In functions that were never ever mentioned officially…

So if I can’t use wait and delay what do I replace it with?
A Custom Heartbeat Wait?

8 Likes

If you need really reliable timing, yes, use a Heartbeat-based wait.

For spawning threads reliably see Crazyman32’s comments above.

3 Likes

Coroutines can only be used once. I’m not sure if it’s the same for spawn as I refrain from using it. Coroutines are probably most useful for your uses.

edit: ran once

if i made a mistake please cut me some slack, i don’t code much with lua.

That sounds like a huge glaring flaw with the budgeter more than anything.

2 Likes

spawn destroys the stack trace the same as coroutines do. Errors that occur after a yield on a new thread will get a new call stack. spawn is always after a yield, which means it always destroys the trace. So this can’t be seen as an advantage of spawn, because they both have this behavior. Never use spawn.

My Promise library has a custom scheduler built-in for Promise.delay. This is what I use in my games if I want to wait an amount of time. The Promise pattern isn’t for everyone, but if you haven’t heard of it I think you should at least look into it.

4 Likes

honestly 90% of the time people use coroutines or spawns, could be made into simple bindable events and functions hooked up to them (even one time functions, they can be disconnected)
i love bindables, other than that i usually use coroutines, heard spawn is just old and never really experimented with it

3 Likes

@buildthomas You mentioned spawning medium amounts of functions/threads. But would performance be negatively impacted by one or two being spawned lightly (what I mean is not very often)?

@ashcide You can run coroutines more than once.
Many guys just go for:

coroutine.wrap(function()
 --ETC
--action happened
coroutine.yield()
end)()

As that’s quick and easy (and doesn’t emit an error message when you forget coroutine.yield() in it)
But other things can be like:

local f = coroutine.create(function() 
                               --ETC 
                           end)
coroutine.resume(f)
--action happened
coroutine.yield(f)
--action happened
coroutine.resume(f)

The one issue I have with coroutine is that I never know if the wrapped ones I make are deleted when they’re yielded/errored.


Looks like the new trend for programmers nowadays is to skip using the old architecture. Might as well make my own personal lib of things I need from Run Service like pause, etc.

Though I will say that maybe the engineers should do something as it would make our lives a little easier if wait() was just hooked up to runservice instead of old tech. That way we wouldn’t need to reference it in every script if we needed reliable threading.

3 Likes

You won’t run into issues with that few amount of threads, but why maintain bad habits? :slightly_smiling_face:

1 Like

Can you please go into more detail on why we shouldn’t use wait() besides “use My Promise library”

13 Likes

As noted previously, spawn waits for 1/30th of a second (equal to wait()) whereas coroutine.create/resume does not. Coroutine.wrap however when called will yield the parent thread until the child coroutine yields or returns a result meaning wrapping does not function as an instantaneous replacement for spawn.

Here is an example of a minimal “fast spawn” implementation (one that does not yield):

local function fastSpawn(func, ...)
	local thread = coroutine.create(func)
	coroutine.resume(thread, ...)
	return thread
end

Coroutines have tons of applications where they may not seem fit. Coroutine.wrap will create a c function which is important to note when creating environments disguised as real Roblox environments. Coroutines also block any environment bubbling (and debug information sadly) from getfenv.

Here’s some more extremely useful coroutine utility stuff: (My favorite is the Pointer function!)

local function Pointer(...) -- A "pointer" to a vararg. Performant, small, and stupidly useful!
	local func = coroutine.wrap(function(func, ...) -- Hold the vararg and wrapped function (to return via yield)
		coroutine.yield(func) -- Yield the wrapped function
		return ... -- Return the full vararg once resumed again
	end) -- Create a wrapped function to hold the vararg in memory
	return func(func, ...) -- Call the function (to start the coroutine and literally hold the vararg in memory)
end
-- Example of above:
local myPtr = Pointer("a", "b", 123)

print(myPtr()) -- Prints each variable ("a", "b", 123)
-- Output: a b 123

local function cwrap(func) -- Wraps a function so it behaves exactly like it did previously but it is now wrapped within a coroutine (and thus appears like a c function to Roblox and user code)
	return coroutine.wrap(function() -- A wrapper function
		local results = Pointer() -- Empty pointer for function results
		while true do
			results = Pointer(func(coroutine.yield(results()))) -- Repeatedly yield results pointer and call a function with custom args
		end
	end)
end
-- Currently, this is most likely impossible for any modern exploits to escape from. Don't go around using this for client security though >:(

local func = cwrap(function(...)
	print(...)
	return true
end)
func("123") -- Prints 123, returns true
func("abc") -- Prints abc, returns true
14 Likes