Coroutines - When and how to use them

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:

  1. ‘suspended’ - Either just created or is yielding from coroutine.yield, eitherall waiting to be resumed.
  2. ‘running’ - Currently resumed and is running code inside.
  3. ‘normal’ - Resumed but awaiting another Coroutine to stop yielding
  4. ‘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.

  1. 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!
  1. 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:

  1. coroutine.running - Returns the current running Coroutine or nil if there isn’t any (thread object.)

  2. coroutine.status - Returns the status of the passed Coroutine, can be either one of the statuses earlier in the tutorial.

  3. 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:
Image
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

218 Likes

Are there any situations where create/resume would be better than wrap? Ever since I migrated from spawn to coroutines I’ve only ever found wrap useful.

8 Likes

As I said previously, resume can return the boolean if it succeeded.
This can be helpful for debugging your coroutined functions, as wrap only supplies the error string.

3 Likes

This is a pretty good tutorial! It gives a great general knowledge of coroutines, and additional info that might be helpful, such as the notion of a thread. Two notes though, that I hope are considered constructive:

  1. Since this article is all about coroutines, which is one subject in the see of many subjects, it’s considered good work to cover EVERYTHING about coroutines. You did a splendid job with that, although there might be bits and pieces missing. For example, you could’ve mentioned how coroutines are used as iterator functions, as the pil states. Also, it seems that you didn’t cover a lot of stuff about coroutine.yield(), how it yields the coroutine, until coroutine.resume()'d again, you talked about it theoretically rather than giving examples. Or how coroutine.yield() gets any additional passed arguments when coroutine.resume() is called, and actually returns them, or the fact that you can use return inside of a coroutine to actually return something. Consider adding these!

  2. The code examples you give in this article are pretty good, because they’re unusual in a way, and not commonly seen. That’s good, but at the same time might be a problem for beginners. For example:

local Thread = coroutine.create(print);
print(Thread); --// thread: <hexadeciamal memory adress>
coroutine.resume(Thread, "Hello, World!") --// Hello, World!

When you pass print as an argument, that might end up being confusing for some people, so you may wanna explicitly explain it to them. It would’ve been nicer if you introduced print as a second example, rather than a first, so it can be easily understood later after understanding how normal functions are passed as arguments.

10 Likes

Thanks so much for the tutorial. It covers the necessary things to start working with coroutines, and that’s a good thing since a lot of users have trouble using them. Thanks!

3 Likes

Thanks for the feedback and I agree with your points, I’ve added an example of using yield and coroutines as an iterator.

Also, I can see what you mean as most people will see print called and not passed like a function - to replace that I’ve used a function which prints for now.

2 Likes

Overall nice tutorial though I feel you should add that coroutines obliterate your stack traces (from my knowledge, feel free to correct me). You can still fetch them though.

A little mistake here:

local Thread = coroutine.wrap(Write);
print(Thread); --// thread: <hexadeciamal memory adress>
Thread(“Hello, World!”) --// Hello, World!

This doesn’t print thread and its address, but the function and its address.

1 Like

The returns of coroutine.yield, coroutine.resume, and the results of calling the function returned by coroutine.wrap aren’t mentioned.

When a coroutine is resumed with coroutine.resume all extra arguments are either arguments to the thread’s function (if the thread hasn’t started), or results to coroutine.yield. The returns are either false and the error object (if the thread errors), or true and all returns from the thread’s function (if it returned) or all arguments to coroutine.yield.

The function returned by coroutine.wrap works similarly, all arguments to the function are either arguments to the thread’s function (if the thread hasn’t started), or results to coroutine.yield. The results are either an error (if the thread errors) or all returns from the thread’s function (if it returned) or all arguments to coroutine.yield.

If a thread yields with coroutine.yield, the next time the thread is resumed it returns from the yield with all arguments from coroutine.resume returned from the yield. All arguments to coroutine.resume are returns for coroutine.resume.

coroutine.running returns nil if the thread is the main thread (in 5.1), not sure what you mean by if there isn't any (thread object). In any case, you shouldn’t have access to the main thread in roblox.

I think a better description is a running thread which has resumed another coroutine which is currently running, not sure what stop yielding means.

I think this should be phrased as returned from the function, reached the end of the function seems ambiguous.

When the thread is resumed the next time, it returns from coroutine.yield and continues, resulting in the thread ending. (and there is a missing ) too)

local Thread = coroutine.create(function(Number)
    while true do
        print(Number)
        Number = Number+coroutine.yield(Number)
    end
end)
coroutine.resume(Thread,1) --> 1
coroutine.resume(Thread,2) --> 3
local Success,Result = coroutine.resume(Thread,5) --> 8
print(Success) --> true
print(Result) --> 8

coroutine.wrap propagates any errors in the thread.
main thread would be more accurately described as the thread resuming the other thread, since something other than the main thread can resume other threads.

There is also the function coroutine.isyieldable, which returns whether the running thread can yield.

Also worth mentioning, each thread gets its own stack, so debug.traceback should be used with the thread argument to get the stack of another thread. (can be used to get stack info after the thread errors)

7 Likes

Hi I remember reading maybe a year ago that the spawn, delay functions would always at least wait() before being ran and that they couldn’t fire every frame like stepped, heartbeat or renderStepped. I never liked wait() because I see it unwieldy and was wondering if this changed or I made a mistake because here there is no mention of limitations, even on the recent learn Roblox wiki it states coroutines behave with no more restriction than as if you added an in game script for the task.

yeah spawn and delay do both wait() for the next tick/frame, and thats one of the big differences between spawn/delay and coroutines: coroutines don’t wait() while spawn and delay do.

2 Likes

Those are completely different things and cannot be compared.

pcalls are used for debugging [catching errors] , while coroutines are used for creating multiple threads that can run collaboratively.

2 Likes

Despite being able to debug Coroutines with the return values of resume/wrap, I would recommend to use pcalls for any functions that could error. As @UniversalScripter stated, Coroutines’ task is to create new collaborative threads and not handle errors like pcalls. In terms of efficiency, pcalls are much better to use.

2 Likes

Sorry for bringing this back, I was wondering about whether there can be potential memory leaks with using Coroutines? When they’re done, are they GC’d, or would I have to set the coroutine variable to nil? I can’t find information about that anywhere, wouldn’t want to find out the hard way…

Once the Coroutine is completely finished, everything within the Coroutined function is garbage collected like any other.

For the variable, once the thread has left the scope it’s defined within it’ll be garbage collected.

If you define it in the global scope, it may not be GC’d until it reaches EOF. You might want to use do blocks:

do
    --// Code
end

My answers assume you are defining it locally.

2 Likes

If it’s defined, isn’t it the same as variables and functions? It wouldn’t be GC’d if it’s in the global scope, but it doesn’t actually execute the function infinitely, unless I call it, right? It doesn’t actually run the thread, so, just defining it shouldn’t cause a memory leak, right?

The coroutined function is only ran when the returned function from coroutine.wrap is called or the thread returned from coroutine.create is resumed. No memory leaks could occur here.

3 Likes

Yet another brilliant tutorial by ReturnedTrue! :laughing:

2 Likes

I’d like to refer to an excellent video that demonstrates that concurrency is not parallelism. If either of those terms mean something to you, watching the video will help clear up whether coroutines (either in Lua or Go) are truly designed for parallelism. Note that the video refers to Go, but the concepts apply to Lua too.

1 Like

This has helped a lot! Used to never understand coroutines, now I do!

1 Like