We use cookies on this site to enhance your user experience

Thread Scheduler

Thread Scheduler

Sep 13 2018, 3:06 AM PST 5 min

Roblox uses a thread scheduler to allow multiple scripts to execute in parallel. You can use this to your advantage to run two pieces of code in parallel in the same script. Roblox’s thread scheduler is exposed through the functions spawn, wait, and delay.

For the rest of this article, the word “Task” is used to refer to a coroutine being managed by the thread scheduler.

Queueing tasks

There are two ways to queue tasks into the thread scheduler:

spawn(function()
    print("Spawn - started")
    wait(2)
    print("Spawn - working")
    wait(1)
    print("Spawn - done")
end)
lua|
delay(1, function()
    print("Delay - started")
    wait(2)
    print("Delay - working")
    wait(2)
    print("Delay - done")
end)    

Task switching

When Roblox runs a wait function (or any other YieldFunction), it pauses the current task, queues it up for later, then looks for other waiting tasks (ie another script, or an event handler), and resumes the next most important. There is no other way to switch task - if you’ve queued a task with Spawn, you have to wait() in order to make it run:

Additionally, new tasks can be run when a queued task terminates - with no wait() in the above code:

ypcall

ypcall queues a task, and then pauses the current task until either the queued task completes or errors

Implementation details

The following code shows a Roblox thread scheduler written in lua that (seems to) behave in the same way as the builtin one:

function wait(time)
    enqueue_task{coro=coroutine.wrap(fn), at=tick() + time}
    return coroutine.yield()
end

function spawn(fn)
    enqueue_task{coro=coroutine.wrap(fn), at=tick()}
end

function delay(time, fn)
    enqueue_task{coro=coroutine.wrap(fn), at=tick() + time}
end

local tasks = {}
function enqueue_task(t)
    -- there are more efficient ways to insert and sort, but oh well...
    tasks[#tasks + 1] = t
    table.sort(tasks, function(a, b) return a.at < b.at end)
end

-- The main scheduler
while true do
    local now = tick()
    if #tasks ~= 0 and tasks[1].at <= now then
        local t = table.remove(tasks, 1)
        coroutine.resume(t.coro, now - t.at, now)
    end
end

This does not cover:

  • Bare coroutine.yield() behaving like wait
  • pcall

See also

  • Articles/Beginners Guide to Coroutines - These are used internally by the thread scheduler
Tags:
  • optimization
  • coding
  • performance