This tutorial is mainly made for clarifying misconceptions around coroutine/task
This tutorial is split into 2 parts:
Coroutine
We should begin with this function:
local function foo(...) : boolean
print(...)
return true
end
In which we return a boolean and print arguments passed through. But what if we want to have multiple return statements? We use coroutines, specifically using coroutine.yield
to replace the default return:
local function foo(...) : boolean
local args = table.pack(...)
coroutine.yield(args[1])
coroutine.yield(args[2])
coroutine.yield(args[3])
end
local thread = coroutine.create(foo)
print(coroutine.resume(thread,1,2,3)) -- 1
print(coroutine.resume(thread,1,2,3)) -- 2
print(coroutine.resume(thread,1,2,3)) -- 3
Confused?:
You probably knew that coroutines aren’t exactly simulating parallel processing. If not well that is the first one to be debunked. The second myth is that they are executing in shuffle. That is also wrong. As a matter of fact they are executing in the exact same manner as any other function with the only added ability of keeping it’s execution position after return(yield)
Where else can we use the ability of keeping execution position after return? To simulate static variables:
local getStaticVar = coroutine.wrap(function()
local static = 0
while true do
static += 1
coroutine.yield(static)
end
end)
print(getStaticVar()) -- 1
print(getStaticVar()) -- 2
print(getStaticVar()) -- 3
Note:
All of the examples above can be done alternatively without the usage of coroutines. But where is the fun in that?
Summary
Table below makes an analogy to standard functions:
Function | Equivalent |
---|---|
status | Original |
isyieldable | Original |
create | local function foo() end |
yield | local function foo() return end |
resume | local function foo() end foo() |
wrap | local function foo() end foo() |
close | local function foo() foo = nil end |
running | local function foo() foo = foo end |
Task (without the parallel luau functions)
Task is a more interesting library than coroutines due to them being directly tied to the resumption cycle, but underhood it works exactly the same as coroutines, which is proven by the fact that all of the task functions can be implemented manually through default coroutines.
The most popular function of the task library is task.wait
, which sometimes is used like:
while task.wait(1) do
-- do work
end
or
while true do
-- do work
task.wait(1)
end
Note:
All of the task functions use
coroutine.yield
in one way or another
Great use, and the only thing I would suggest is that if you call task.wait
without providing arguments, then instead use RunService.Heartbeat:Wait()
as it is a more direct approach without the overhead of the task internal code.
The second most popular is task.spawn
, which is, as a matter of fact, equivalent to coroutine.resume
. Same goes for task.delay
being an equivalent to task.wait
and task.cancel
being an equivalent to coroutine.close
For task.defer
it is a different story. This one allows you to put the code inside at the end of the resumption cycle. Of which is global or local scope? I am not sure exactly, but the fact that this is the only original function of the task’s timing control remains
Summary
task is mainly made to replace and pack outdated: wait
,spawn
and delay
globals into a single library to make timing control a lot more noticeable in the code. If you are working with threads, my personal opinion would be to not use the task library in it’s entirety and instead use coroutines. All of the task functions can be implemented manually without any issues, with the only exception being task.defer
.
EDIT 1: Added links to content list