Question about threads

So I’ve done some research and well I haven’t found much about this so let me directly ask a few questions:

  1. Do events / listeners automatically create new threads?
  2. Do functions create new threads? (Remote Functions and bindables)
  3. Does requiring module multiple times a bad idea?

About question 3, I’ve ran tests in studio with the command bar and a module script, for the most part if I run the command once running a function in the module twice it will wait for the first time to finish

If I run the code twice, it’ll run twice even with the same module but not using the same require.

So does require create new threads? Should I be requiring multiple times?

I have a lot of modules and normally within the function I’d use a spawn to make a new thread when there is a noticeable loop that will yield the rest of the script.

EDIT: Sorry this post isn’t really focused and sentences are all over the place.

4 Likes
  1. No
  2. No
  3. No

You should be using corutines over spawn because they are garenteed to run. Roblox’s ‘threads’ are actually just running based on time and are not actual new threads. Using spawn or corutines allows you to run different code at ‘the same time’ without you having to care about threads. Another way of putting it is that you can use them when you dont want your script to wait for a part of your script to finish. You can call hundreds of simultanious Roblox ‘threads’ and nothing will go wrong.

Requiring modules multiple times is not bad. Just remember that it shares information. If you require in one script and change a local in the module, it will change the local for every other script requiring that module. Try not to require a module multiple times in the same script though.

Yes, a listener does get called in a new thread, the above post is incorrect.

They are actual threads. Parallelism is not the same as concurrency. They are not “threads”.

Module return values get cached for the computer requiring them, so the second require of the same module will return the same value as was returned by the first require. Clients have separate caches.

Never, under any circumstance, use the spawn function, use coroutines instead. This is because spawn will delay execution of the code for at least 1/30th of a second (because Roblox’s task scheduler runs 30 times a second, think of it as an additional unnecessary wait()), and they are unreliable (also avoid the delay function, wait function, and Debris service for similar reasons).

Horror stories about spawn if you don’t believe me:

7 Likes

I was talking about threads in terms of cpu threads. Roblox uses a time based system where things can run in concurently. Aka Roblox ‘threads’.

Yes, my point was that they are real threads, no different from any other. Parallelism is a subset of concurrency. Paralellism refers to parallel (at the same time) execution.

See:

1 Like

What I was trying to say is that spawn/coroutines do not link directly to seperate cpu threads. But you gave a clearer definition probably.

Every connection gets its own thread, yes. The threads are re used if no yield occured.

require caches the result from a module script, and yes when you require a module it creates a new thread.

BindableEvents and RemoteEvents create threads since to listen to them you have to create a connection.
BindableFunctions and RemoteFunctions create new threads. (threads should be re used when no yield occurs like connections)

spawn is bad since it waits before creating the thread.

spawn(function()
    print("a")
end)
coroutine.wrap(function()
    print("b")
end)()
-- Output
--> b
--> a

Since modules cache the result, its not a terrible idea, but it certainly doesn’t help to require a module multiple times.

7 Likes

You’ve stated that I should avoid delay and debris which caught my attention. I use these 2 a lot and I mean like majority of my script. If I shouldn’t use delay what should I use? Coroutine + wait?

If I were to avoid these I’d have no method of destroying certain things after a certain time without new threads and a wait.

coroutine.wrap() would be your general replacement for spawn().

For replacing wait(), you’d write a simple thread scheduler that uses RunService.

Yes, it is really necessary to do something like this if you don’t want awful bugs in your game. There’s nothing more Roblox than having to write your own abstractions over the engine’s bad ones. If you don’t believe me, see the above linked thread (and more successful developers can attest to this).

ModuleScript example of a simple task scheduler I wrote from memory and did not test:

local ExecutionScheduler = {}

local Services = {
    Run = game:GetService("RunService"),
}

local Schedule = {}

local function Update()
    local CurrentTime = tick()

    for Task in pairs(Schedule) do
        if Task.TimeToRun >= CurrentTime then
            coroutine.wrap(Task.Function)(unpack(Task.Arguments))
            Schedule[Task] = nil
        end
    end
end

function ExecutionScheduler.Schedule(Function, Delay, ...)
    local Task = {
        Arguments = {...},
        Function = Function,
        TimeToRun = tick() + Delay,
    }
    Schedule[Task] = true

    return Task
end

function ExecutionScheduler.Unschedule(Task)
    Schedule[Task] = nil
end

function ExecutionScheduler.Start()
    Services.Run.Stepped:Connect(Update)
end

return ExecutionScheduler
5 Likes

I’ve never thought about this, extremely good idea. So this would be more beneficial over wait() and delay() am I correct? Also would this be a good replacement for Debris?

EDIT: I use debris a lot, sometimes 100 times within a second when it comes to visual effects. I know your module creates a thread to execute the function and I know that creating multiple threads in short period is not a good idea.

Yep. A Debris:AddItem(Object, 30) call would just look like ExecutionScheduler.Schedule(function() Object:Destroy() end), 30). Not quite as short, but will reliably work.

With wait(), delay(), and Debris functions, you are specifying to wait at least the number of seconds you want. If you call wait(5), that could theoretically take 10,000 seconds. With a scheduler like the one I posted, every single frame there will be a check, guaranteed.

Creating 100 threads in the scheduler I posted would be fine, especially since a coroutine isn’t created until we need to execute the function. The real problem is creating lots of threads with the evil functions I mentioned.

What do you mean by evil functions?

I normally use Debris to remove tweens, parts and etc. My script would look like this:

for i = 1, 100 do
local p = Instance.new("Part")
Debris:AddItem(p, 5)
end

Also what do you mean by

Wouldn’t it still create 100 threads?

EDIT: Or does it do something similar to this:

thread()

wait(x)
instance:Destroy()
instance2:Destroy()
instance3:Destroy()

end)

Like a stacking function will it will use only 1 thread to delete all these items with the same destroy time.

I meant wait(), delay(), spawn(), Debris:AddItem(), there could be some I’m forgetting. I wouldn’t allow any code like you posted into a game, though

I’d never allow code like that in a project I was programming. You can ignore my advice, and it will appear like stupid advice until you have a game with some minimal level of popularity.

Yes it does, although the thread creation takes place only when we need to execute, vs. the evil functions where it would take place immediately. Roblox’s thread scheduler uses the same code under the hood that coroutines use under the hood.

The performance benefit is kind of complicated and not always present (the reliability benefit is much more important). Basically, since coroutine creation is relatively expensive (keyword relatively), it can be beneficial to spread this out over time. If we create 200 threads, and we want 100 of them to execute at CurrentTime + 10, and another 100 to execute at CurrentTime + 20, we are only creating 100 threads at each of those times, rather than 200 all at once. With creating all of those with delay(), we are immediately creating 200 threads all at once, not even considering the reliability problems.

An analogy would be if we want to use Lua to count from 1 to 1,000,000,000. If we have to do this all at once, our computer will freeze. But if we could do this over 1,000,000,000 seconds, we still have to do the same amount of operations, but the work has been divided so that we never do a huge amount of work all at once.

Of course, if all 200 threads get scheduled for CurrentTime + 10, there’s no performance benefit, but the reliability benefit is important.

If we wanted to improve performance further with a more advanced scheduler, we could add a queue for thread creation and create N threads per frame, instead of creating threads at the time of execution.

3 Likes

Haha, the script I provided was an example but I do use those evil functions. It seems like I need to change my habit of scripting once more. I current have a project with around 50 players on and it does seem pretty laggy. Thanks this will definitely change what I’m going to be doing in the future.

EDIT: I’m glad I asked this question

What about using task.task function?

2 Likes