A coroutine is basically a separate thread, and the coroutine library provides useful wrappers to interact with these threads.
A thread is basically an object that allows the running of code. On any given Luau VM (virtual machine), there is one main thread. In Roblox, this is tied to the ScriptContext service. Any other threads are subthreads of this main thread.
So how is this relevant? Well, there’s an important distinction between threads and the coroutine library. Coroutines are threads. Your scripts are threads. The coroutine library you see in Luau just provides wrappers around the Luau C API to allow you to interact with these threads.
I’ll break them down one by one:
coroutine.create
- This creates a new thread from the main thread (wraps
lua_newthread) and pushes the given function to the top of it’s stack, ready for resumption. The thread is left in the yield state, meaning it is not running currently but can be “resumed” to run. You could say you’ve loaded everything you need for the thread, but it’s paused.
coroutine.resume
- This resumes, or “unpauses” a yielded thread (wraps
lua_resume). It’ll pass the extra arguments given yto coroutine.resume as return values of coroutine.yield from within the thread’s work.
coroutine.yield
- This yields (“pauses”) a thread (wraps
lua_yield). Any arguments you pass to coroutine.yield will be sent back to the code which resumed the thread before.
coroutine.isyieldable
- You’re basically asking the Luau VM, “hey, is this thread safe to yield?” (wraps
lua_isyieldable). It’s basically the Luau VM telling you whether or not it’s safe to call coroutine.yield on the thread.
coroutine.running
- All this does is return the currently running thread.
coroutine.wrap
- This will construct a new thread with the given function, then return a function that when called will resume the new thread, with the given arguments.
local f = coroutine.wrap(function()
print("hi from another thread")
end)
f() --resumes the new thread
ok, but what about the task library?
- The task library and the coroutine library are both built around threads, just the behaviour of the task library is slightly different. For example,
task.spawn will create a thread but will then immediately resume it, as opposed to coroutine.create which will leave it yielded. The task library is built more around the Roblox task scheduler - providing functions like delay and defer to control when threads are executed more seamlessly. Note that functions like coroutine.resume will yield your current thread until the thread you resumed yields, but functions like task.spawn will just resume the other thread alongside your current one.
and yes - this means you can mix and match coroutine and task functions!
Example: coroutine
local function myWork()
local thisThread = coroutine.running() --the thread this function is running on
print(coroutine.status(thisThread)) --> "running"
local arg1, arg2 = coroutine.yield(3, "hi") --pause this thread, return 3 and "hi" to
--the thread which resumed this one
--when this thread is next resumed, arg1 and arg2 will be whatever else was passed
--to coroutine.resume
print(arg1, arg2) --> 5 "hi" in this case
end
--create a new thread in yielded status, and push myWork to the top
--of it's stack
local newThread = coroutine.create(myWork)
--run myWork on the new thread. coroutine.resume will
--yield this thread until that thread yields or finishes.
local success, result1, result2 = coroutine.resume(newThread)
print(success, result1, result2) --> true 3 "hi"
--let's resume the yielded thread with args
coroutine.resume(newThread, 5, "hi")
Example: task
local function myWork()
local thisThread = coroutine.running()
print(coroutine.status(thisThread)) --> "running"
print("you can put work to run alongside your other thread in here.")
end
local thread = task.spawn(myWork)
Example: Mixing the two
--you CAN use task.defer and pass arguments
--into that directly, which will pass them to
--myWork as parameters. This is for demo purposes.
local function myWork()
local arg1, arg2 = coroutine.yield() --task.spawn immediately resumes the thread so we'll yield it
print(arg1, arg2) --> "hi" 3
print("hello from this thread")
local newArg = coroutine.yield(3)
print(newArg) --> 5
end
--let's create the thread with task.spawn
local thread = task.spawn(myWork)
print(coroutine.status(thread)) --> "suspended"
--since we immediately yielded it, we can give it
--arguments through coroutine.resume
local success, returnVal = coroutine.resume(thread, "hi", 3)
print(returnVal) --> 3
--the other thread yielded again. Let's resume it with
--task.spawn, because you can resume a thread as well
--as a function through task.spawn
task.spawn(thread, 5)
I hope this helps, if you have any questions please ask.