Mastering Lua Coroutines: Lightweight Cooperative Multitasking Explained
Learn how Lua coroutines let you write non‑blocking code in a single thread, with step‑by‑step examples, common pitfalls, and verification tips.
21 Nov 2021, 18:15 UTC

Why Use Coroutines in Lua?
Coroutines give you a simple, stack‑based way to write cooperative multitasking code in Lua. Instead of spawning OS threads, you can pause a function with coroutine.yield() and resume it later with coroutine.resume(). This lets you build state machines, generators, and non‑blocking I/O loops while keeping everything in a single thread.
Creating and Resuming Coroutines
A coroutine is created with coroutine.create(function). The function runs only when you call coroutine.resume(). The first call executes the function until the first yield (or return). Each subsequent resume continues from the last yield point.
Basic Example
-- counter.lua
local function counter(name, limit)
for i = 1, limit do
print(name .. ": yielding " .. i)
coroutine.yield(i)
end
return "done"
end
local c1 = coroutine.create(function() counter("C1", 3) end)
local c2 = coroutine.create(function() counter("C2", 5) end)
-- Alternate resuming
while coroutine.status(c1) ~= "dead" or coroutine.status(c2) ~= "dead" do
if coroutine.status(c1) ~= "dead" then
local ok, val = coroutine.resume(c1)
if ok then print("C1 yielded", val) end
end
if coroutine.status(c2) ~= "dead" then
local ok, val = coroutine.resume(c2)
if ok then print("C2 yielded", val) end
end
end
Running this script produces interleaved output, proving that each coroutine remembers its local state across yields.
Error Handling
Unlike coroutine.wrap, coroutine.resume returns two values: a boolean success flag and the first result or error message. Always check the flag to catch runtime errors.
local ok, err = coroutine.resume(c1)
if not ok then
print("Coroutine failed:", err)
end
Example: inserting a division by zero inside the coroutine will make resume return false and the error string.
Common Pitfalls and How to Avoid Them
Ignoring the Resume Result
If you ignore the boolean return from resume, a crash inside the coroutine will silently abort the coroutine, leaving your program in an inconsistent state.
Using coroutine.wrap in Production
coroutine.wrap hides the error inside a protected call, making debugging hard. Prefer resume where you can inspect the error explicitly.
Unintended Side‑Effects in Shared State
Coroutines share the same global environment and can modify shared tables. Ensure that each coroutine has its own private data or uses encapsulated objects to avoid race conditions.
Limitations of Lua Coroutines
No Preemption and Blocking
Coroutines are cooperative. If a coroutine never yields, it will block the entire Lua interpreter. Use coroutine.yield at safe points, especially in long loops.
C Stack Recursion Limits
Coroutines reuse the C stack, so deep recursion inside a coroutine can hit the underlying stack limit. Keep recursion depth shallow or use iterative patterns.
Single‑Threaded Execution
Coroutines cannot be resumed from another OS thread. If you need true parallelism, use Lua's os.execute or external libraries like Lua Lanes.
Verifying Coroutine Behavior
Step‑by‑Step Test Script
Write a script that creates two coroutines, each yielding a counter, and resume them alternately. Observe the interleaved output to confirm state persistence.
Checking Errors
Inject a deliberate error (e.g., 1/0) and call coroutine.resume. Verify that the returned boolean is false and the error message is printed.
Ensuring State Persistence
After a yield, resume the coroutine and confirm that local variables retain their values. This confirms that the coroutine's stack frame is preserved.
When Coroutines Aren't Enough
If you need preemptive multitasking or true concurrency, consider Lua's async libraries (e.g., Lua Lanes) or running separate Lua interpreters in OS threads. Coroutines are ideal for I/O loops and simple state machines but not for heavy parallel workloads.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.