Help With Understanding Threads and Multithreading

So I was looking at this post: Coroutines - When and how to use them and I have a few questions:

‘Lua is a single-threaded programming language’
Does that mean that lua can only execute 1 script/thread (if script and thread are the same thing, not sure) at a time?

‘These are non preemptive and can only be stopped by the Coroutined code, nothing from the outside can stop a Coroutine from running. Keep in mind when any Coroutine or the main thread calls a blocking operation the entire program cannot continue and therefore it can’t be a real alternative to multi-threading.’
Not really to sure what the difference between multi-threading and coroutines is, whats the difference? Not even sure what multi-threading is and coroutines are exactly.

Thank you if you can get to me :grinning:

20 Likes

Threads could be understood like this:

-- Normal coding (Single thread)
local part = Instance.new("Part", workspace)
part.BrickColor = BrickColor.new("Bright red")
while wait(1) do
      print("Hi!")
      part:Clone()
end
-- The script will stop running until that loop is broke since its a wait loop.

Now this is how a different thread is:

local myPart = Instance.new("Part", workspace)
myPart.BrickColor = BrickColor.new("Bright red")

coroutine.wrap(function()
      while wait(1) do
         print("Hi")
         myPart:Clone()
      end
end)()
print("Hello")
print("Hey")
-- These 2 prints are called instantly because the wait loop that is supposed to stop the code until its broke is inside another thread.

LUA can execute multiple scripts at the same time, but not multiple codes at the a single script at the same time.

In the first example, if you copy that same code and create 10 different scripts with the same code, they will be ran normally but the wait loop will still stop the code inside the script.

10 Likes

There’s a bit to this.

Multi-Threading

A “thread” in programming is essentially a set of instructions. A modern operating system will give each set of instructions some CPU time and alternate between them, giving the illusion that they’re running simultaneously. Most computers these days also have multiple CPU cores, meaning that in some sense, some of these threads are actually being run simultaneously. The ability to run several threads simultaneously is known as multi-threading.

Generally, when you’re working with threads, there’s no telling when the operating system will interrupt a thread and switch to a different one, so your code normally has to be written in a way where it can’t break even if one part of your code is being interrupted and another is given time to run. Some code will break if accessed from multiple threads simultaneously. Code that doesn’t break when being accessed by multiple threads at once is called “thread-safe”, and not everything has this quality.

When a thread is interrupted for something else, that thread is said to be “yielding”, i.e. it’s stopped temporarily to allow other things to happen. Threads can yield voluntarily, or can be forced to yield by the operating system.

You can also normally force a thread to abort prematurely, even if the code on it hasn’t finished running.

A lot of what I just said applies to programming in general. On Roblox, things are a bit different.

Coroutines

Lua, by default, doesn’t support multi-threading. Well, it sort-of does, but it leaves a lot to the developer. Coroutines are a somewhat-implementation of multi-threading, and a coroutine is a “thread”.

Coroutines are always created from a function, using coroutine.create(). That coroutine is stopped by default, and you can call coroutine.resume() to start that coroutine. That coroutine will then run until it yields, and once it does, coroutine.resume() will return. This all sounds very confusing, so perhaps an example will explain it better:

function myCoroutine()
	print("3rd")
	coroutine.yield()
	print("5th")
end

print("1st")
local co = coroutine.create(myCoroutine)

print("2nd")
coroutine.resume(co)

print("4th")
coroutine.resume(co)

print("6th")

Let’s go through that step by step:

  • The script prints “1st”, and creates the coroutine. This doesn’t actually run any additional code, so it prints “2nd”.
  • The script resumes the coroutine, and it starts at the beginning of myCoroutine, and prints “3rd”
  • The function calls coroutine.yield(), which exits the coroutine and allows the original script to resume. It prints “4th”.
  • The script resumes the coroutine again, and it continues where it left off, and prints “5th”.
  • The coroutine reaches the end of the function, so the script jumps back and prints “6th”.

This all runs the script one-by-one, so you’re not actually running any code simultaneously. But, just like an operating system switching between different threads very quickly, it gives you the illusion of doing two things at once.

For another example, you could do something like:

function routine1()
	while true do
		print("Routine 1!")
		coroutine.yield()
	end
end

function routine2()
	while true do
		print("Routine 2!")
		coroutine.yield()
	end
end

local co1 = coroutine.create(routine1)
local co2 = coroutine.create(routine2)

while true do
	coroutine.resume(co1)
	coroutine.resume(co2)
	wait()
end

Which will make it appear like both infinite loops are running at the same time. In reality, they’re just being run one by one very quickly. Roblox, at its core, does something similar. It quickly switches between your scripts and gives each some time to run, but it runs them all one by one. This is what the first part of your post means:

You can run multiple scripts “at once”, but Roblox actually runs them all sequentially. They don’t truly run simultaneously. (This is subject to change in Q4, separate hardware threads are on the roadmap. This means you can run Lua core on multiple CPU cores, meaning it’ll truly run simultaneously).

Your 2nd question ties into this as well:

Since Lua by default doesn’t support true multi-threading, there is no way to stop it from the outside once it has been started. It only ever stops if the code inside the coroutine calls coroutine.yield(), or the function finishes running.

This also means that in Lua, all “threads” only yield voluntarily. Lua doesn’t arbitrarily abort a thread at some random interval to return to it later. Roblox made some changes to this and made the engine kill a script that hasn’t voluntarily yielded for too long, but this exit is permanent unlike normal threads.

The Roblox-Yield

There’s coroutine.yield(), but Roblox actually implemented a different type of yield as well. This yield occurs any time you call an “Async” function, or wait for an event, or just call wait(). It behaves similarly to a yield caused by coroutine.yield() in that it pauses a coroutine and causes coroutine.resume() to return to the normal code.

When a coroutine “roblox-yields”, the code will automatically resume some time later. This is unlike coroutine.yield(), which requires you to manually resume the coroutine, even if the remaining code is still running. It essentially causes the coroutine to run “simultaneously” with the rest of the script (albeit not truly simultaneously as discussed earlier).

98 Likes

Thank you for the detailed reply! Helped a ton :grinning:. However, I do have a few questions:

does that simply mean that there has to be no syntax errors?

What does this mean, do you mind giving me an example?

Thats all, thank you so much for the detailed post and have a great day!

2 Likes

I’ll satisfy your curiosity, but keep in mind none of this is relevant on Roblox at the moment because there is no true multi-threading yet. Separate hardware threads are on the roadmap for Q4 this year, but I suspect even then this will only be relevant if you explicitly try to run multiple threads.

No, this means the code could behave unexpectedly even if there are no errors at all. When you’re working with code that runs truly asynchronously, you have to be careful. You have to consider that your code may be interrupted at any point, even at points where you might not expect it to be.

Take the following function as an example:

local count = 0
function imagineThisIsAThread()
	local newValue = count + 1
	count = newValue
end

Now say we run this twice, each on a separate hardware thread. Because we run the function twice, we expect count to equal 2 when all is said and done. There are two ways this can go:

  • The first thread executes, and then the second one does. Neither thread is interrupted, so the code runs as expected and the result is 2.
  • The first thread starts and calculates newValue = 1, and is interrupted at that point. The second thread then starts and also calculates newValue = 1, because the first thread hasn’t changed count yet. Then the first and second thread both set count to 1. In this case, count is equal to 1.

You see the problem: Both threads executed the code correctly but, because they access shared memory in an uncontrolled way, they won’t always give you the correct result.

Keep in mind this is a simplified example and that normally even expressions like count = count + 1 (or even count++; in other languages) are error-prone. This is because they have to retrieve the value, perform the calculation, and store the new value, all while the thread could be interrupted at any of those steps.

To handle these cases, you usually are given functions that handle accessing shared memory safely (so-called “atomic functions”) or some other way to synchronize code execution for when you need to parallelize non-thread-safe code.

Hopefully this helps!

26 Likes

Hey, this is super late but in this scenario would it be more optimal/efficient to use task.spawn if your only using it to run in a new thread, I’ve been wondering this