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

I would use coroutine.create() and coroutine.resume() rather than spawn() because spawn waits the same time as wait(). That’s also because coroutine is better than spawn is my opinion and my coding style prefers coroutines over spawn.

Make sure you read this article very closely before choosing your approach:

4 Likes

So, after some experimentation and investigation, from what I can tell coroutine and spawn are pretty much equal in terms of performance. The only real difference is that spawn has a fixed budget (0.1s) and threads can only execute every 1/30s at most.

That said, if you use coroutines, you still need to be wary of blowing your implicit frame budgets as well or FPS will drop. So, I’m not sure you’ve really won that much.

In fact, found I had to create a custom task scheduler. Not a hard task, and I definitely prefer mine to the one I can’t control. So, if the win is that you’re forced to write your own custom task scheduler - ok, agreed. Always good for folks to know what they’re doing.

That said - I think spawn for folks who are not doing much processing per second and don’t want to use their own custom task scheduler with frame budgets, it actually works pretty well. Do keep an eye on those values returned by wait() however. wait() lag is less visible than FPS lag so it can sneak up on you as per above. In particular, don’t do more processing than 0.1s per second or 0.015ms per frame, though there is actually flexibility in that TBH. Use the microprofiler to verify what’s happening is what you expect.

Also, to be clear, the problem isn’t so much “threads” (coroutines, whatever) but rather processing time in the threads. I can get up to 500 threads per frame (1/60s) assuming I don’t do anything in the threads. More logic in the threads would reduce this accordingly to fit the 0.015s frame budget.

For this reason, I’m a bit confused by the story above and how you were seeing 15s lag with spawn but when you switched them to coroutine you didn’t see any fps drop.

1 Like

coroutines are the way to go. There have been several instances of large-scale games (namely Vesteria) that used to use spawn() for threading; sometimes there would be up to a 15-second delay in the spawn() function actually begin to run. This is because spawn() only runs the next time that Roblox’s task scheduler updates.

3 Likes

I’d like to see the specific technical details behind that. There must have been a frightful amount of processing for that to happen. I can schedule up to over 100K threads (executing every 3 seconds) via spawn with no wait lag.

Perhaps that was a pre-optimized Lua VM problem?

You can try it here, use microprofiler to verify, click on the top button.

image

612 call counts * 60 is about 36000 different threads called per second, 100K different threads over 3 seconds.

Using spawn for absolutely all threading scenarios is obviously not going to work. Using it correctly for the right scenarios makes perfect sense.

3 Likes

Yes, has anyone here that in the past observed the 15s delays tried any of that old code
post-introduction of Luau? It would be good to know if spawn() is not so bad now.

1 Like

Edit: this doesnt work with code that yields so nvm

Possible solution to overcome this

local function Run(Function, ...)
	return coroutine.wrap(Function)(...)
end

local function Spawn(Function, ...)
	local Success, Response = pcall(Run, Function, ...)

	if (not Success) then
		warn(tostring(Response))
		warn(debug.traceback())
	end
	
	return {Success = (Success and true or false), Response = Response}
end

Code i used to test, idk why its not formatting properly

Summary
local function Run(Function, ...)

return coroutine.wrap(Function)(...)

end

local function Spawn(Function, ...)

local Success, Response = pcall(Run, Function, ...)

if (not Success) then

warn(tostring(Response))

warn(debug.traceback())

end

return {Success = (Success and true or false), Response = Response}

end

local function Test1()

print("Example Error")

error("Example Error")

end

local function Test2(...)

print(...)

return ...

end

local function Test3()

print(2 + "a")

end

local Test1Response = Spawn(Test1)

print(Test1Response.Success, Test1Response.Response)

local Test2Response = Spawn(Test2, 1, 2, 3)

print(Test2Response.Success, Test2Response.Response)

local Test3Response = Spawn(Test3)

print(Test3Response.Success, Test3Response.Response)
1 Like

@blazepinnaker
I assume you mean create up to that many threads using spawn or coroutine? Or am I not understanding something?

Because I can literally run @gillern’s script that he posted above (with a few modifications :wink: ) 100k times.

local function Run(Function, ...)
	return coroutine.wrap(Function)(...)
end

local function Spawn(Function, ...)
	local Success, Response = pcall(Run, Function, ...)

	if (not Success) then
		warn(tostring(Response))
		warn(debug.traceback())
	end
	
	return {Success = (Success and true or false), Response = Response}
end


function Hello()
	while wait(1) do
		--print("Hello")
	end
end

for i = 1, 10 do -- Create 10,000 threads 10 times using corotine.wrap = 100,000 threads when complete
	wait(1)
	for i = 1, 10000 do
		Spawn(Hello)
	end
	print("Created: "..(i * 10000).." Threads")
end
print("Done")

With zero noticeable render lag.

Or am I not understanding something?

I think this thread has gotten a little over technical and the bigger picture needs to be looked at. Fundamentally, spawn has a huge issue with it that coroutines don’t. As long as you are programming a game responsibly, you won’t have an issue with coroutines - this isn’t true of spawn.

For what it’s worth, whilst coroutines sound scary to newer developers, they aren’t. You can still keep a very simple pattern, basically the same as spawn, with a minor difference, with coroutines.

--These both do the same thing
spawn(function()
   print("Hello, world!")
end)

coroutine.wrap(function()
   print("Hello, world!")
end)()

The only difference is the () at the end of the coroutine.wrap. Thats because coroutine.wrap returns the thread, so it needs to be called like a function to be executed. But you can basically treat it like an extra bit of syntax you just need to get your code to run.

Watch out for it though, because if you miss out the (), then the code won’t run, but it also won’t error.

10 Likes

@DragRacer31 , the test code is fairly straightforward -

for i=1,numberOfThreads,1 do
  spawn(function() while true do doStuff() wait(3) end)
end

For a directly relevant game example, let’s say you want to update a Part built scoreboard in your game every 60 seconds. Or check and change the weather every 240 seconds.

spawn(function()
  while gamePlaying do
    updateScoreboard()
    wait(60)
  end
end

spawn(function()
  while gamePlaying do
    updateWeather()
    wait(240)
  end
end

Coroutine doesn’t do this, and are the scenarios that the OP was asking about.

There have been some attempts in the forums to re-write the out of box Roblox task scheduler, but they suffer from fairness and safety from what I’ve seen.

1 Like

I think discussing whether spawn theoretically hits issues with benchmarks and such is mostly missing the point–many top developers, including myself, have had real world issues where spawns are either horribly delayed or do not run at all.

I wrote a thread on Twitter about it. We get these issues all the time, and recently had to plug up another thing that was still using spawn. Don’t use them.

3 Likes

X doesn’t work for my use case, therefore nobody should ever use X seems like a crazy argument to make. I suspect Roblox itself doesn’t work for many use cases, so nobody should use Roblox?

Physics is another thing. It very very frequently causes issues, so nobody should use Roblox physics?

Better would be to learn and appreciate where a certain API is useful and where it doesn’t work so well, and an alternative is more useful. The API works very well from what I’ve seen, and writing a better thread scheduler is surprisingly hard to do, especially without access to C calls.

Anyhow, I’ll quit at this point, as I’m clearly out numbered by those burned by spawn. Hopefully someone gets Roblox to fix what bugs exist in the API rather than just deprecating it. Debris, delay, etc are very useful calls to have.

2 Likes

It seems more of an assertive comment to steer developers away from encountering issues of their own in real world applications. Sometimes we have issues raised on the forums and a simple switch of functions has been able to cure incidents. Spawn/delay/Debris → coroutines, pairs → ipairs, so on.

I have honestly no clue about this topic since I’m not a technical guy at all (I only know how to write code, not the details behind the implementation of the language), but perhaps this is a discussion you’d like to raise again once multithreaded Lua gets thrown into the equation? It seems very likely that we’ll be getting this in the (near) future. I’d assume upgrading the task scheduler per thread would be part of this.

I don’t assume these functions will be going away anytime soon because they have their respective utility towards developers and newer developers, but the way they work right now just isn’t a right fit for a number of developers. In time as the engine improves and we see more improvements to the language (already having some nice things such as typing), perhaps the argument will change.

3 Likes

So, one of my things with spawn is that it yields the script.
Some scripts I worked with used spawn but spawn messed up the script as spawn was yielding the script instead of just going along.

I’ve personally never used spawn but I prefer coroutine because it runs alongside the code, its as you created an entirely other script. Coroutine is more used for like loading markers, like you would want the user to know how many assets are left to load, so you would put that into a coroutine while you initalize the menu and stuff while the coroutine updates the amount of assets left.

Coroutine allows CPU threading within one script

One solution works for every use case (fast spawn, coroutines), and one is the same thing but objectively worse in every metric and provably causes issues even at small usages (spawn).

3 Likes

Coroutines seems to silence my errors within the given scope. Anyway, I think it depends on the way in which you use spawns. If spawn is used sparingly, it causes no unforeseen problems.

2 Likes

Mind elaborating on what you mean when you stated that spawns cause issues at small usages?

I agree. I’m not one to usually disagree with “top developers” but these kinds of arguments are redundant. I myself have never encountered issues using spawn versus coroutines but from a beginners prospective I wouldn’t want to use coroutines from what I’ve observed. Coroutines fail to error out to the output when code inside it errors , instead the thread is terminated and the code simply never runs with no indication as to why (at least in module scripts for me). With spawn I have no problems at all because the code errors and is seen in the output (again from a beginners prospective this is). In any case spawn is handled by the thread schdular and due to there being a yield inside it ( a wait() type of yield iirc) there is complications on the execution time of your thread since it could get throttled. Now I’m not saying spawns are the best at all, but if used sparingly and if you know your use cases for it, I don’t think you’ll encounter the infamous problems that Builthomas and Everaa encountered. (Forgive me if some things don’t make sense or it’s not grammatically correct, I’m on mobile atm).

I don’t use coroutines as an alternative to spawn precisely because of the lack of error handling. I use fast spawn, which is what I consistently have recommended.

local function FastSpawn(callback)
    local event = Instance.new("BindableEvent")
    event.Event:Connect(callback)
    event:Fire()
    event:Destroy()
end

This preserves errors, fires immediately, and is objectively better than spawn.

Even if used sparingly, the timings of spawn are still undocumented, and can be pushed to unreasonable wait times at any time. Even just core scripts can muck with the thread scheduler. Plus, why bother making sure you’re not using spawn against some completely unknown, arbitrary constraint when you can use fast spawn to zero issue.

5 Likes

Alright I see what you mean with the use of fast spawns over other methods. Again my only issue with you guys reporting your problems with spawn is that I’m not able to accurately replicate your problems to see them for myself. I’m the sort of person who isn’t easily convinced of something that I can’t replicate myself, mind showing me a way in which I can replicate the problems that you’ve encountered with spawn? Also, I get that you guys are claiming it’s an arbitrary threshold that I might reach but again my question to that is, I’ve used spawn with proper implementation in various parts of my code and have come to no conclusion of it getting throttled for “unreasonable wait times” as what you guys claim. Now, if you ask me my take on fast spawns my only issues with that is it’s redundant and time consuming to write every time I need to get a new thread. Sure I could go ahead and create a wrapper module for threading but then again I don’t write code for no reason, I gotta see some tangible proof of my current way of implementation.

Personally, I think it all comes down to implementation. I get you can replicate the wait() functions problems as done so in this twitter thread and I can see spawn falling victim through rapid usage which I can consider improper implementation since knowing spawn has that additional yield, you should be aware of and use it respectfully. I have again had no issues with spawn so far but when I do need to do threading on a larger scale I will consider coroutines or better yet fast spawns if I encounter the problems you guys have had so far. But for my current use case spawn seems to be doing just fine. But again, I’m simply asking for some tangible steps to follow to find the arbitrary throttling of spawn functions without looping 100+ times and purposefully exploiting the issues it has that’s previously mentioned.

1 Like