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:
coroutine.createmakes a coroutine in the suspended state.coroutine.resumestarts (or resumes) the coroutine — runs untilyieldorreturn.coroutine.yieldsuspends the coroutine — admits returning values to the resumer.coroutine.resumereturnstrue, <yielded values>(orfalse, <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.wrapfor iterator-style use. - Use
coroutine.create/resume/yieldfor 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.