Short summary on threads (coroutines/task)

This tutorial is mainly made for clarifying misconceptions around coroutine/task

This tutorial is split into 2 parts:

  1. coroutines
  2. task

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

2 Likes