Polyglot
Languages Lua concurrency
Lua § concurrency

Coroutines

Lua’s coroutines are cooperatively-scheduled stackful suspensions — admit substantial generator-style and asynchronous patterns. The principal functions: coroutine.create (create a coroutine from a function), coroutine.resume (start or continue), coroutine.yield (suspend), coroutine.status (query state), coroutine.wrap (wrap as iterator). Coroutines are not threads — Lua does not include native multi-threading; coroutines run on a single OS thread, switching only at explicit yield/resume points. The combination — cooperative coroutines, the asymmetric resume/yield model, the coroutine state machine, the substantial generator-and-async use cases — is the substance of Lua’s concurrency story. For true parallelism, embedding into a multi-threaded host application or using libraries like Lua Lanes admits substantial parallelism.

Coroutine basics

local co = coroutine.create(function()
    print("start")
    coroutine.yield(1)
    print("after first yield")
    coroutine.yield(2)
    print("end")
    return 3
end)

print(coroutine.resume(co))                        -- "start"; true, 1
print(coroutine.resume(co))                        -- "after first yield"; true, 2
print(coroutine.resume(co))                        -- "end"; true, 3
print(coroutine.resume(co))                        -- false, "cannot resume dead coroutine"

The mechanism:

  1. coroutine.create makes a coroutine in the suspended state.
  2. coroutine.resume starts (or resumes) the coroutine — runs until yield or return.
  3. coroutine.yield suspends the coroutine — admits returning values to the resumer.
  4. coroutine.resume returns true, <yielded values> (or false, <error> on error).

Coroutine states

local co = coroutine.create(function()
    coroutine.yield()
end)

print(coroutine.status(co))                        -- "suspended"

coroutine.resume(co)
print(coroutine.status(co))                        -- "suspended" (yielded)

coroutine.resume(co)
print(coroutine.status(co))                        -- "dead"

The states:

  • suspended — newly created or yielded.
  • running — currently executing.
  • normal — has resumed another coroutine.
  • dead — completed or errored.

Passing values

yield returns whatever the next resume passes:

local co = coroutine.create(function()
    print("first run with", coroutine.yield())     -- yields nothing initially; receives next resume's args
    print("second run with", coroutine.yield(10))  -- yields 10; receives next resume's args
    return 20                                       -- final return
end)

print(coroutine.resume(co))                        -- starts; first yield returns true with no value
print(coroutine.resume(co, "a", "b"))              -- "first run with a b"; yields 10; returns true, 10
print(coroutine.resume(co, "x", "y"))              -- "second run with x y"; returns true, 20

The mechanism admits substantial bidirectional communication.

coroutine.wrap

The wrap admits using a coroutine as a function:

local fn = coroutine.wrap(function()
    for i = 1, 5 do
        coroutine.yield(i)
    end
end)

print(fn())                                        -- 1
print(fn())                                        -- 2
print(fn())                                        -- 3
print(fn())                                        -- 4
print(fn())                                        -- 5
-- print(fn())                                      -- ERROR: cannot resume dead coroutine

The wrap:

  • Returns a function that resumes the coroutine.
  • Returns the yielded values directly.
  • Propagates errors (rather than returning false/error).

The mechanism is conventional for iterators:

for i in coroutine.wrap(function()
    for n = 1, 5 do
        coroutine.yield(n)
    end
end) do
    print(i)                                       -- 1, 2, 3, 4, 5
end

pcall and coroutines

The coroutine.resume catches errors:

local co = coroutine.create(function()
    error("oops")
end)

local ok, err = coroutine.resume(co)
print(ok, err)                                     -- false, "input.lua:2: oops"

The form is substantially pcall-like — the coroutine’s error is admitted as a return value of resume.

Generator pattern

Coroutines are conventional for generators:

local function range(start, stop, step)
    step = step or 1
    return coroutine.wrap(function()
        for i = start, stop, step do
            coroutine.yield(i)
        end
    end)
end

for i in range(1, 10) do print(i) end              -- 1 to 10
for i in range(10, 1, -2) do print(i) end          -- 10, 8, 6, 4, 2
-- Fibonacci:
local function fibonacci()
    return coroutine.wrap(function()
        local a, b = 0, 1
        while true do
            coroutine.yield(a)
            a, b = b, a + b
        end
    end)
end

local fib = fibonacci()
for _ = 1, 10 do
    io.write(fib(), " ")
end
-- 0 1 1 2 3 5 8 13 21 34

Custom iterators via coroutines

local function pairs_sorted(t)
    local keys = {}
    for k in pairs(t) do keys[#keys + 1] = k end
    table.sort(keys)

    return coroutine.wrap(function()
        for _, k in ipairs(keys) do
            coroutine.yield(k, t[k])
        end
    end)
end

local config = {host = "localhost", port = 8080, debug = true}
for k, v in pairs_sorted(config) do
    print(k, v)
end
-- debug    true
-- host     localhost
-- port     8080

Treated in Iterators.

Async patterns

Coroutines admit async-like patterns when combined with an event loop:

-- Conceptual async pattern (requires event-loop integration):
local function async(f)
    return function(...)
        local co = coroutine.create(f)
        local function resume(...)
            local ok, err = coroutine.resume(co, ...)
            if not ok then error(err) end
        end
        resume(...)
    end
end

local function await(operation)
    local co = coroutine.running()
    if not co then error("await must be called from a coroutine") end
    operation(function(result) coroutine.resume(co, result) end)
    return coroutine.yield()
end

-- Usage:
async(function()
    local data = await(function(callback)
        fetch_async("https://example.com", callback)
    end)
    print("got:", data)
end)()

The mechanism admits substantial integration with callback-based APIs — coroutines as async/await.

For production async, libraries like copas, cqueues, luv (libuv binding) admit substantial event-loop integration.

Tasks vs coroutines

A subtle distinction:

  • Coroutines — language primitive; cooperative; explicit yield/resume.
  • Threads — OS-level; preemptive; concurrent.

Lua’s standard library admits only coroutines; true threads require:

  • Lua Lanes — multi-state Lua threading.
  • LuaJIT FFI + pthreads — substantial low-level threading.
  • Embedding into a multi-threaded host — typical in game engines.

Common patterns

Sequence generator

local function naturals()
    return coroutine.wrap(function()
        local n = 1
        while true do
            coroutine.yield(n)
            n = n + 1
        end
    end)
end

local n = naturals()
print(n(), n(), n())                               -- 1, 2, 3

Mapping iterator

local function map_iter(iter, fn)
    return coroutine.wrap(function()
        for v in iter do
            coroutine.yield(fn(v))
        end
    end)
end

for x in map_iter(range(1, 5), function(n) return n * 2 end) do
    print(x)                                       -- 2, 4, 6, 8, 10
end

Filter iterator

local function filter_iter(iter, pred)
    return coroutine.wrap(function()
        for v in iter do
            if pred(v) then
                coroutine.yield(v)
            end
        end
    end)
end

for x in filter_iter(range(1, 10), function(n) return n % 2 == 0 end) do
    print(x)                                       -- 2, 4, 6, 8, 10
end

Take

local function take(iter, n)
    return coroutine.wrap(function()
        local count = 0
        for v in iter do
            if count >= n then return end
            coroutine.yield(v)
            count = count + 1
        end
    end)
end

for x in take(naturals(), 5) do print(x) end       -- 1 to 5

Pipeline

local function pipe(input, ...)
    local steps = {...}
    local current = input
    for _, step in ipairs(steps) do
        current = step(current)
    end
    return current
end

local result = pipe(
    range(1, 100),
    function(it) return filter_iter(it, function(n) return n % 2 == 0 end) end,
    function(it) return map_iter(it, function(n) return n * n end) end,
    function(it) return take(it, 5) end
)

for x in result do print(x) end                    -- 4, 16, 36, 64, 100

Producer-consumer

local function producer()
    return coroutine.wrap(function()
        for i = 1, 10 do
            coroutine.yield(i)
        end
    end)
end

local function consumer(prod)
    while true do
        local item = prod()
        if item == nil then break end
        process(item)
    end
end

consumer(producer())

Cooperative scheduling

local tasks = {}

local function spawn(f)
    tasks[#tasks + 1] = coroutine.create(f)
end

local function run_all()
    while #tasks > 0 do
        local remaining = {}
        for _, co in ipairs(tasks) do
            local ok, err = coroutine.resume(co)
            if not ok then
                print("error:", err)
            elseif coroutine.status(co) ~= "dead" then
                remaining[#remaining + 1] = co
            end
        end
        tasks = remaining
    end
end

spawn(function()
    for i = 1, 3 do
        print("task A", i)
        coroutine.yield()
    end
end)

spawn(function()
    for i = 1, 3 do
        print("task B", i)
        coroutine.yield()
    end
end)

run_all()
-- task A 1
-- task B 1
-- task A 2
-- task B 2
-- task A 3
-- task B 3

The mechanism admits substantial cooperative scheduling without OS threads.

State machine

local function make_traffic_light()
    return coroutine.wrap(function()
        while true do
            for _, color in ipairs({"red", "yellow", "green"}) do
                coroutine.yield(color)
            end
        end
    end)
end

local light = make_traffic_light()
print(light())                                     -- "red"
print(light())                                     -- "yellow"
print(light())                                     -- "green"
print(light())                                     -- "red"

Backtracking

Coroutines admit substantial backtracking (e.g., parser combinators):

local function permutations(t)
    return coroutine.wrap(function()
        if #t == 0 then
            coroutine.yield({})
            return
        end
        for i = 1, #t do
            local rest = {}
            for j = 1, #t do
                if j ~= i then rest[#rest + 1] = t[j] end
            end
            for sub in permutations(rest) do
                local p = {t[i]}
                for _, v in ipairs(sub) do p[#p + 1] = v end
                coroutine.yield(p)
            end
        end
    end)
end

for p in permutations({1, 2, 3}) do
    print(table.concat(p, ","))
end
-- 1,2,3
-- 1,3,2
-- 2,1,3
-- 2,3,1
-- 3,1,2
-- 3,2,1

Bidirectional communication

local co = coroutine.create(function(initial)
    local x = initial
    while true do
        x = coroutine.yield(x * 2)
        if x == nil then break end
    end
end)

print(coroutine.resume(co, 5))                     -- true, 10
print(coroutine.resume(co, 7))                     -- true, 14
print(coroutine.resume(co, 100))                   -- true, 200
print(coroutine.resume(co, nil))                   -- true, (no value; coroutine ends)

A note on coroutine.running

local co = coroutine.running()
-- In main: returns nil (or main thread, ismain=true in 5.2+)
-- In coroutine: returns the coroutine

The function admits substantial introspection — useful for “is this running in a coroutine?” checks.

A note on errors

Errors in a coroutine do not crash the program — they are returned by resume:

local co = coroutine.create(function()
    error("inside coroutine")
end)

local ok, err = coroutine.resume(co)
print(ok)                                          -- false
print(err)                                         -- "inside coroutine"

The mechanism admits substantial error isolation between cooperative tasks.

A note on the conventional discipline

The contemporary Lua coroutines advice:

  • Use coroutines for generator-style iteration.
  • Use coroutine.wrap for iterator-style use.
  • Use coroutine.create/resume/yield for substantial control.
  • Use coroutines for async patterns — combine with event loops.
  • Trust tail calls — substantial recursion in coroutines is admitted.
  • Don’t expect threads — coroutines are cooperative, not preemptive.
  • Use Lua Lanes or similar for substantial parallelism.
  • Embed into multi-threaded hosts for substantial concurrent scenarios.

The combination — cooperative coroutines, the asymmetric resume/yield model, the substantial generator and iterator patterns, the coroutine-as-async basis, the absence of native threads — is the substance of Lua’s concurrency surface. The discipline produces substantial sequential code with cooperative suspension; the trade-off is the absence of substantial parallelism without external libraries or host integration.