Which Fast Spawn variant do you prefer?

Which Fast Spawn Variant do you use?

What I mean by Fast Spawn is creating a new thread using various methods as mentioned below;

  • Coroutine
  • Heartbeat
  • BindableEvent

0 voters

  • Coroutine

    • Fastest but destroys the stack trace and obscure errors.
  • Heartbeat

    • Fast but has a Delay and becomes slower depending on work load.
  • BindableEvent

    • Slowest and not Code based but reliable.

Reference: https://devforum.roblox.com/t/coroutines-v-s-spawn-which-one-should-i-use/368966



Each variant is useful in their own way but I would like to study more about each of them so I can choose the best for any occasion.

My Opinion;

I probably wouldn’t use the Heatbeat variant because it’s speed is not dramatically different from the BindableEvent variant and it could become slow, additionally I really prefer the Coroutine variant over the BindableEvent variant because of the differences in speed but the down side is reliability in terms of debugging but I don’t know why your code would be prone to errors especially the part where you are using Fast Spawns.

Feel free to share your thoughts on the topic

5 Likes

BindableEvents and the Heartbeat event have nothing to do with spawn. You shouldn’t be using either for the sole purpose of creating a new thread. The thread you linked mentions how a Heartbeat is better than wait() because it’s guaranteed to resume, but as far as I can tell it doesn’t say anything about them being used in the same context as coroutines.

1 Like

Can’t you use pcall to catch errors within Coroutines?
Coroutines masterrace?

3 Likes

The use cases I have for the need of a separate thread are far and few on Roblox. Most of my work with threading in roblox stems from networked stuff which kind of has to be in a RemoteEvent (I know that wasn’t mentioned, but for all intents and purposes, it’s a Bindable).

In what few cases I do use separate threads, I just use spawn() since the delay doesn’t matter all too much for me. If there’s any edge cases, I stray to coroutines, and generally it’s for if I need to yield for a specific reason or something like that, something that can leverage the API coroutines offer. It is incredibly rare for me to use these, however, and I don’t think any of my production scripts make use of them at all. That’s a lot of code, mind you. I think FastCast is the only outlier here because I needed to thread it and the 1/30s delay would throw off results too harshly.

Now if Lua were as robust as C#, I’d probably be using async (or whatever its equivalent was) a lot more because under many circumstances it is very powerful. Remotes/Bindables offer similar functionality to C#'s await keyword by allowing use of the :Wait() method of connections, but it’s not quite the same.

1 Like

BindableEvent by far. Like I believe it’s almost not debatable.

6 Likes

I prefer coroutines however I propagate the stack trace down using debug.traceback(). That way I can still debug errors.

Edit: This should probably be in the Developer Discussion category rather than scripting support.

Very interesting, would you be so kind to give us an example code?

I’ll definitely look into that, thank you for sharing.

It was at first but new members wouldn’t be able to reply or vote on the poll.

Bindables do have a limitation where you can’t fire recursively more than 200 times, but I never get close to that.

2 Likes

Very interesting!

would you mind sharing example code so we can fully understand what you meant by “fire recursively”, is the code below accurate?

Function; Function = function()
	Function()
end
Function()

I honestly didn’t even know that there was a limit to firing them recursively. Thanks for the info. :slight_smile:

Here’s a simplified version of the “SafeCall” module I made for my projects.

While it’s possible to Connect/Disconnect methods from events while firing to get a fresh traceback, this implementation is faster while also allowing tables to be passed without serialization. This is significantly faster than alternatives that create/destroy BindableEvents upon calling.

This module is a great alternative to pcall if you just need to call something safely but don’t want to silence errors (hence “SafeCall”.)

local globalMethod;
local globalArgs = {}
local globalArgsLen = 0
local onFire = function()
	globalMethod(unpack(globalArgs, 1, globalArgsLen))
end

local fireDepth = 0
-- This cache could use __mode = "v" or some other cleanup mechanism, but it isn't very significant.
local bindableEventCache = setmetatable({}, {__index = function(self, i)
	local event = Instance.new("BindableEvent")
	event.Event:Connect(onFire)
	-- Could potentially add the same event to the cache 4-ish times before hitting problems.
	self[i] = event
	return event
end})

local SafeCall = function(f, ...)
	-- Would be smart to assert that f is callable here to prevent mysterious errors in 'onFire'.
	--  I removed that because my code referenced a debug-only module used for making that assertion.
	globalMethod = f
	
	local argsLen = select("#", ...)
	for i = 1, argsLen do
		globalArgs[i] = select(i, ...)
	end
	
	-- Clean up previous args during recursion
	for i = argsLen + 1, globalArgsLen do
		globalArgs[i] = nil
	end
	
	globalArgsLen = argsLen
	
	-- Fire
	fireDepth = fireDepth + 1
	bindableEventCache[fireDepth]:Fire()
	fireDepth = fireDepth - 1
	
	-- Clean up
	globalMethod = nil
	for i = 1, globalArgsLen do
		globalArgs[i] = nil
	end
	globalArgsLen = 0
end

Now let’s test the recursion limit:

local function test(depth)
	if depth > 256 then
		print("Done!")
	else
		print("Depth", depth)
		SafeCall(test, depth + 1)
	end
end
test(1)

As you can see, it errors after a depth of 200:

This can be solved by falling back to coroutine.wrap if fireDepth gets too high, but I have never hit the limitation.

EDIT:
This implementation can get pretty close to coroutine.wrap in terms of performance.

Here are some rough performance results comparing SafeCall with coroutine.wrap, Nevermore’s fastSpawn, and a SafeCall variation that localizes BindableEvent.Fire:

6 Likes

I personally use coroutines because heartbeat and bindables aren’t all that useful for running code on multiple threads. I use bindables for communication between two of the same scripts (or for it’s :Wait() function as an alternative to doing wait()) and Heartbeat for a very short wait.

2 Likes