Lua Coroutines Guide — Cooperative Multitasking and Yield
In this tutorial, you will learn about Lua Coroutines Guide. We cover key concepts, practical examples, and best practices to help you master this topic.
Lua coroutines are threads of execution that yield control explicitly -- they pause at a Coroutine.yield() call and resume from the same point later, enabling cooperative multitasking without race conditions.
What You'll Learn
- Creating coroutines with coroutine.create
- Yielding and resuming execution
- Passing data between coroutines
- Using coroutines for iterators and state machines
Why It Matters
Coroutines let you write non-blocking code without callbacks or complex state machines. Durga Antivirus Pro uses Lua coroutines to scan multiple files cooperatively without thread overhead. Game engines use coroutines for animation sequences, AI behaviors, and cutscenes that need to pause and wait.
Real-World Use
A Love2D game uses a coroutine for a boss fight sequence that cycles through attack patterns with delays. A network service uses coroutines to handle multiple simultaneous connections without threads. A unit test framework uses coroutines to simulate timeouts and async operations.
flowchart LR
A["Coroutines"] --> B["Create"]
A --> C["Resume"]
A --> D["Yield"]
A --> E["Status"]
B --> F["Producer-Consumer"]
A:::current --> B
style A fill:#2563eb,stroke:#2563eb,color:#fff
style B fill:#dbeafe,stroke:#2563eb,color:#1e40af
style C fill:#dbeafe,stroke:#2563eb,color:#1e40af
style D fill:#dbeafe,stroke:#2563eb,color:#1e40af
style E fill:#f1f5f9,stroke:#94a3b8,color:#64748b
Coroutine Basics
-- Create a coroutine
local co = coroutine.create(function()
print("Coroutine started")
coroutine.yield()
print("Coroutine resumed")
return "done"
end)
print(coroutine.status(co)) --> suspended
-- First resume starts execution
coroutine.resume(co) --> Coroutine started
print(coroutine.status(co)) --> suspended (yielded)
-- Second resume continues from yield
coroutine.resume(co) --> Coroutine resumed
print(coroutine.status(co)) --> dead
Passing Data In and Out
local co = coroutine.create(function(max)
local sum = 0
for i = 1, max do
sum = sum + i
-- Yield the current partial sum
coroutine.yield(sum)
end
return sum -- final return
end)
-- Each resume returns values from yield
local _, partial = coroutine.resume(co, 10)
print(partial) --> 1
local _, partial = coroutine.resume(co)
print(partial) --> 3
local _, partial = coroutine.resume(co)
print(partial) --> 6
-- Final return
local _, final = coroutine.resume(co)
print(final) --> nil (already consumed)
-- Last resume with final value
-- ... continue calling
Coroutine States
local co = coroutine.create(function()
coroutine.yield("first")
coroutine.yield("second")
return "final"
end)
print(coroutine.status(co)) --> suspended (not started)
coroutine.resume(co)
print(coroutine.status(co)) --> suspended (yielded)
coroutine.resume(co)
print(coroutine.status(co)) --> suspended (yielded again)
coroutine.resume(co)
print(coroutine.status(co)) --> dead
-- Resuming a dead coroutine returns false + error
local ok, err = coroutine.resume(co)
print(ok) --> false
print(err) --> cannot resume dead coroutine
Producer-Consumer Pattern
-- Producer coroutine generates values
local function producer()
return coroutine.create(function()
local i = 0
while true do
i = i + 1
coroutine.yield(i)
end
end)
end
-- Consumer takes values from producer
local function consumer(prod, count)
for _ = 1, count do
local _, value = coroutine.resume(prod)
print("Consumed:", value)
end
end
local prod = producer()
consumer(prod, 5)
Expected output:
Consumed: 1
Consumed: 2
Consumed: 3
Consumed: 4
Consumed: 5
Coroutine as Iterator
Coroutines can implement powerful iterators with internal state.
-- Iterator that generates Fibonacci numbers
local function fibonacci(max)
return coroutine.wrap(function()
local a, b = 0, 1
while a <= max do
coroutine.yield(a)
a, b = b, a + b
end
end)
end
-- Use as a for-in iterator
for n in fibonacci(50) do
print(n)
end
Expected output:
0
1
1
2
3
5
8
13
21
34
coroutine.wrap
coroutine.wrap returns a function that resumes the coroutine -- simpler but less powerful.
local function counter()
local i = 0
return coroutine.wrap(function()
while true do
i = i + 1
coroutine.yield(i)
end
end)
end
local next = counter()
print(next()) --> 1
print(next()) --> 2
print(next()) --> 3
Game Animation Example
local function fade_in_sprite(sprite, duration)
return coroutine.create(function()
local steps = 10
local step_duration = duration / steps
for alpha = 0, 1, 1 / steps do
sprite.alpha = alpha
coroutine.yield(step_duration)
end
sprite.alpha = 1
end)
end
local function boss_battle_sequence()
return coroutine.create(function()
print("Boss appears!")
coroutine.yield(2) -- wait 2 seconds
print("Boss attacks!")
coroutine.yield(1) -- wait 1 second
print("Boss is vulnerable!")
coroutine.yield(3) -- wait 3 seconds
print("Boss enrages!")
return "phase_change"
end)
end
Error Handling
Errors inside coroutines are caught by coroutine.resume.
local co = coroutine.create(function()
error("something went wrong")
end)
local ok, err = coroutine.resume(co)
print(ok) --> false
print(err) --> something went wrong
-- The coroutine is now dead
print(coroutine.status(co)) --> dead
Common Mistakes
1. Resuming a dead coroutine
Once a coroutine finishes (returns) or errors, it's dead. Resume returns false, error. Check status before resuming.
2. Forgetting that yield cannot occur in protected calls
Inside pcall, a yield across the pcall boundary raises an error. Lua 5.4 improved this but still has limitations.
3. Using resume return values incorrectly
coroutine.resume returns success first, then the values from yield. Always check the first return value.
4. Creating too many coroutines
Coroutines are lightweight but not free. Each has a stack. For thousands of concurrent tasks, consider other patterns.
5. Yielding inside metamethods
Calling coroutine.yield inside certain metamethods (like __index, __gc) is not allowed and raises an error.
Practice Questions
1. What is the difference between coroutines and threads? Coroutines are cooperative -- they yield explicitly. Threads are preemptive -- the OS scheduler can interrupt them at any time. Coroutines share the same OS thread with no race conditions.
2. What does coroutine.wrap do?
It creates a coroutine and returns a function that resumes it. Each call to the function resumes the coroutine until it yields or finishes.
3. What happens when you resume a dead coroutine?
coroutine.resume returns false followed by an error message. The coroutine cannot be restarted.
Challenge: Write a coroutine-based producer that generates prime numbers up to a limit, and a consumer that prints them. Use coroutine.wrap for the producer.
FAQ
{{< faq question="Can coroutines run in parallel?" >}} No. Lua coroutines are single-threaded. Only one coroutine runs at a time, and they yield cooperatively. True parallelism requires Lua threads (not available in standard Lua) or LuaJIT's low-level primitives. {{< /faq >}}
{{< faq question="What is the stack limit for coroutines?" >}}
The default stack size for a Lua coroutine is about 30KB. You can set a custom stack size with coroutine.create in Lua 5.4+. Deeply nested calls may exceed this limit.
{{< /faq >}}
{{< faq question="Can I yield from within nested function calls?" >}} Yes, as long as the entire call chain is within the coroutine. A yield will suspend the entire coroutine, not just the innermost function. {{< /faq >}}
{{< faq question="How do I pass initial arguments to a coroutine?" >}}
Pass them to the first coroutine.resume call: coroutine.resume(co, arg1, arg2). The coroutine function receives these as its parameters.
{{< /faq >}}
{{< faq question="Are coroutines used in real Lua applications?" >}} Yes. OpenResty (Nginx + Lua) uses coroutines extensively for handling concurrent HTTP requests. Love2D games use coroutines for cutscenes and animations. Redis Lua scripts use them indirectly through the pcall/xpcall mechanism. {{< /faq >}}
Try It Yourself
local function task(name, steps)
return coroutine.create(function()
for i = 1, steps do
print(name .. ": step " .. i)
coroutine.yield()
end
print(name .. ": complete!")
end)
end
local task1 = task("Task A", 3)
local task2 = task("Task B", 2)
-- Cooperative scheduling
while coroutine.status(task1) ~= "dead" or coroutine.status(task2) ~= "dead" do
if coroutine.status(task1) ~= "dead" then
coroutine.resume(task1)
end
if coroutine.status(task2) ~= "dead" then
coroutine.resume(task2)
end
end
Expected output:
Task A: step 1
Task B: step 1
Task A: step 2
Task B: step 2
Task A: step 3
Task A: complete!
Task B: complete!
What's Next
Now that you understand coroutines, learn about file I/O -- reading and writing files in Lua.
| Topic | Description | Link |
|---|---|---|
| File I/O | Read, write, and Process files | {{< ref "09-file-io" >}} |
| Modules | require, module patterns, packages | {{< ref "10-modules" >}} |
| Python Async | Compare coroutines with Python async | Python |
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro