Introduction
Co-routines, formally known as Coroutines, can be used to create a separate non preemptive threads and run code without yielding the main thread. But what exactly are threads, and in what cases would you need to use them?
In this tutorial, I’ll be covering the entirety of Coroutines and when you’ll need to use them so let’s begin!
Understanding the concept of threads
The Computer Science meaning of a thread is line of execution usually a component of a process that can be managed by a scheduler, and in Roblox the Task Scheduler, which are run on the CPU. When the thread yields, usually in Roblox due to a wait()
, the thread will not continue until it’s woken back up by the scheduler.
Imagine an analogy of a person being a thread and their alarm clock being the scheduler, they do their tasks but when they need to sleep the alarm clock will wake them up again so they can continue doing their tasks. Multi-threading opens up the possibility of there being two or more people doing their tasks however when one person sleeps the other person is not affected and can still carry on doing what they’re doing.
Differences between a Coroutine and a proper thread
Keep in mind, Coroutines are not actual threads. Lua is a single-threaded programming language with Coroutines being the replacement of multi-threading. They’re a type of collaborative multi-threading when a Coroutine yields another Coroutine or the main thread receieves control again and continues.
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.
Coroutine statuses
Coroutines have a range of statuses:
- ‘suspended’ - Either just created or is yielding from
coroutine.yield
, eitherall waiting to be resumed. - ‘running’ - Currently resumed and is running code inside.
- ‘normal’ - Resumed but awaiting another Coroutine to stop yielding
- ‘dead’ - Has errored or reached the end of the function, cannot be resumed
These can come in handy and will need to be remembered for later on.
Creating a Coroutine
Creating a coroutine is very simple and can be achieved by two methods from the coroutine
library.
- create/resume
coroutine.create
creates a thread object from the passed function, this can then be resumed with coroutine.resume
of which you can pass arguments into:
local Write = function(Message)
print(Message);
end
local Thread = coroutine.create(Write);
print(Thread); --// thread: <hexadeciamal memory adress>
coroutine.resume(Thread, "Hello, World!") --// Hello, World!
- wrap
An alternative and more versatile method is coroutine.wrap
which returns a function that resumes the Coroutine, of course when calling it you can pass arguments:
local Write = function(Message)
print(Message);
end
local Thread = coroutine.wrap(Write);
print(Thread); --// function: <hexadeciamal memory adress>
Thread("Hello, World!") --// Hello, World!
Honestly, I prefer wrap other create/resume but it’s up to you what you use.
When you’ll need to use Coroutines
Of course it isn’t so clear when these would be needed, however they can be essential in some Scripts.
In most mini-game styled games there’s a main while loop which handles the rounds etc, of course this yields and code after it cannot run. This is where a Coroutine comes in, you can wrap the while loop into a Coroutine:
coroutine.wrap(function()
while true do
--// Code
wait(2);
end
end)() --// Don't forget to call it!
print("Hello, World!") --// Prints all fine
Or asynchronous methods in a class:
local Class = {};
Class.__index = Class;
function Class.new()
return setmetatable({}, Class);
end
function Class:Method()
wait(2)
end
function Class:MethodAsync()
coroutine.wrap(Class.Method)(Class) --// Has to include Class in the arguments since a:b() = a.b(a)
end
local NewClass = Class.new();
Class:MethodAsync();
print("Hello, World!") --// Prints with no delay
Other coroutine methods
There’s other coroutine methods which can be of use to you such as:
-
coroutine.running
- Returns the current running Coroutine or nil if there isn’t any (thread object.) -
coroutine.status
- Returns the status of the passed Coroutine, can be either one of the statuses earlier in the tutorial. -
coroutine.yield
- Yields the current Coroutine this is called inside and passes the arguments given to the next time it’s resumed, this will not continue until resumed. The use of this within a while loop could mean that you can use the coroutine repetitively without it becoming dead. An example of usage:
local Thread = coroutine.create(function(Number)
while true do
print("Current Number:", Number);
coroutine.yield(Number + 1);
end
end)
coroutine.resume(Thread, 1) --// Current Number: 1
coroutine.resume(Thread) --// Current Number: 2
coroutine.resume(Thread) --// Current Number: 3
Using coroutines as iterators
With the use of coroutine.yield
you can infact use Coroutines as a custom iterator. Since the passed arguments to yield get receieved again when resumed you could possibly pass the current index in the table.
A custom iterator for only BaseParts would look like:
local function BasePartIterator(Table)
local Length = #Table;
local Thread = coroutine.create(function(_, Index)
if (not Index) then --// If we're not passed an Index, make it 1;
Index = 1;
else
Index = Index + 1; --// Otherwise increase it
end
for i = Index, Length do --// From the current Index to the Length
if (Table[i]:IsA("BasePart")) then
coroutine.yield(Table[i], i); --// These will be passed back again next iteration
end
end
--// If none is found then it'll return nil, nil stops the for loop iterating
end
return function() --// Iterator
local Success, BasePart, Index = coroutine.resume(Thread)
return BasePart, Index;
end
end
local WorkspaceDescendants = workspace:GetDescendants();
for BasePart, IndexFound in BasePartIterator(WorkspaceDescendants) do
print(BasePart:IsA("BasePart")); --// Always true
print(WorkspaceDescendants[i] == BasePart); --// Also always true
end
Handling errors within Coroutines
If a Coroutine was to error, it would not affect the main thread. coroutine.resume
returns just like a pcall would with if it succeeded and the response which is either an error message or the return value of that function.
coroutine.wrap
also does the same as resume, however it doesn’t return the boolean success.
Alternatives to Coroutines entirely
Of course, there’s a few alternatives with spawn()
and a ‘FastSpawn’ implementation. I wouldn’t recommend the use of spawn due to the horror stories it’s receieved (taking up to 15 seconds to run!).
FastSpawn on the other hand, is a pretty good way of manipulating BindableEvents due to their swiftness, a simple implementation of this would be:
local function FastSpawn(Body)
local BindableEvent = Instance.new("BindableEvent");
BindableEvent.Event:Connect(Body);
BindableEvent:Fire();
BindableEvent:Destroy();
end
Keep in mind, if it errors it will affect the main thread.
Roblox & Multi-threading
As in the roadmap Multi-threaded Lua being a target:
Coroutines could be replaced by this, though they could have use for small actions. Stay tuned for this folks.
Resources
There’s more resources online to further your knowledge in Coroutines if you’d like to learn more:
Thanks for reading!
This was my fifth Community Tutorial, hope you did enjoy.
If there’s anything you’d like to ask, or correct, do reply