Lua - Coroutines & Cooperative Multitasking
Coroutines & Cooperative Multitasking
Time: ~35 minutes
Learn Lua's powerful coroutine system for cooperative multitasking, generators, and asynchronous-style programming.
Learning Objectives
- Understand coroutines and cooperative multitasking
- Master coroutine creation, yielding, and resuming
- Build generators and iterators with coroutines
- Implement producer-consumer patterns
- Create asynchronous-style control flow
Introduction to Coroutines
Coroutines are functions that can suspend their execution and resume later, enabling cooperative multitasking.
Basic Coroutine Operations
-- Create a simple coroutine
local function simple_coroutine()
print("Coroutine started")
coroutine.yield("First yield")
print("Coroutine resumed")
coroutine.yield("Second yield")
print("Coroutine finishing")
return "Final result"
end
-- Create coroutine
local co = coroutine.create(simple_coroutine)
-- Check coroutine status
print("Initial status:", coroutine.status(co))
-- Resume coroutine multiple times
local success, result = coroutine.resume(co)
print("First resume:", success, result)
print("Status after first resume:", coroutine.status(co))
local success, result = coroutine.resume(co)
print("Second resume:", success, result)
print("Status after second resume:", coroutine.status(co))
local success, result = coroutine.resume(co)
print("Third resume:", success, result)
print("Final status:", coroutine.status(co))
Expected Output:
Initial status: suspended
Coroutine started
First resume: true First yield
Status after first resume: suspended
Coroutine resumed
Second resume: true Second yield
Status after second resume: suspended
Coroutine finishing
Third resume: true Final result
Final status: dead
Coroutine with Parameters
-- Coroutine that receives and processes data
local function data_processor()
local data = coroutine.yield("Ready for data")
while data do
print("Processing:", data)
local result = string.upper(data)
data = coroutine.yield("Processed: " .. result)
end
return "Processing complete"
end
local processor = coroutine.create(data_processor)
-- Start coroutine
local success, message = coroutine.resume(processor)
print("Start:", message)
-- Send data to coroutine
local success, result = coroutine.resume(processor, "hello")
print("Result 1:", result)
local success, result = coroutine.resume(processor, "world")
print("Result 2:", result)
local success, result = coroutine.resume(processor, "lua")
print("Result 3:", result)
-- End processing
local success, final = coroutine.resume(processor, nil)
print("Final:", final)
Expected Output:
Start: Ready for data
Processing: hello
Result 1: Processed: HELLO
Processing: world
Result 2: Processed: WORLD
Processing: lua
Result 3: Processed: LUA
Final: Processing complete
Generators with Coroutines
Number Sequence Generator
-- Fibonacci sequence generator
local function fibonacci_generator()
local a, b = 0, 1
while true do
coroutine.yield(a)
a, b = b, a + b
end
end
-- Helper function to create iterator from coroutine
local function make_iterator(generator)
local co = coroutine.create(generator)
return function()
local success, value = coroutine.resume(co)
if success and coroutine.status(co) ~= "dead" then
return value
end
return nil
end
end
-- Test Fibonacci generator
print("First 10 Fibonacci numbers:")
local fib = make_iterator(fibonacci_generator)
for i = 1, 10 do
print(i .. ":", fib())
end
-- Prime number generator
local function prime_generator()
local function is_prime(n)
if n < 2 then return false end
for i = 2, math.sqrt(n) do
if n % i == 0 then return false end
end
return true
end
local n = 2
while true do
if is_prime(n) then
coroutine.yield(n)
end
n = n + 1
end
end
-- Test prime generator
print("\nFirst 8 prime numbers:")
local primes = make_iterator(prime_generator)
for i = 1, 8 do
print(i .. ":", primes())
end
Expected Output:
First 10 Fibonacci numbers:
1: 0
2: 1
3: 1
4: 2
5: 3
6: 5
7: 8
8: 13
9: 21
10: 34
First 8 prime numbers:
1: 2
2: 3
3: 5
4: 7
5: 11
6: 13
7: 17
8: 19
Custom Iterator with Coroutines
-- Tree traversal using coroutines
local function create_tree_iterator(tree)
local function traverse(node)
if node then
-- In-order traversal
traverse(node.left)
coroutine.yield(node.value)
traverse(node.right)
end
end
return coroutine.wrap(function()
traverse(tree)
end)
end
-- Create a binary tree
local tree = {
value = 4,
left = {
value = 2,
left = {value = 1},
right = {value = 3}
},
right = {
value = 6,
left = {value = 5},
right = {value = 7}
}
}
-- Traverse tree using coroutine iterator
print("Tree traversal (in-order):")
for value in create_tree_iterator(tree) do
print("Node value:", value)
end
-- Range generator with step
local function range(start, stop, step)
return coroutine.wrap(function()
local current = start
step = step or 1
if step > 0 then
while current <= stop do
coroutine.yield(current)
current = current + step
end
else
while current >= stop do
coroutine.yield(current)
current = current + step
end
end
end)
end
-- Test range generator
print("\nRange 1 to 10 with step 2:")
for value in range(1, 10, 2) do
print("Value:", value)
end
print("\nCountdown from 5 to 1:")
for value in range(5, 1, -1) do
print("Value:", value)
end
Expected Output:
Tree traversal (in-order):
Node value: 1
Node value: 2
Node value: 3
Node value: 4
Node value: 5
Node value: 6
Node value: 7
Range 1 to 10 with step 2:
Value: 1
Value: 3
Value: 5
Value: 7
Value: 9
Countdown from 5 to 1:
Value: 5
Value: 4
Value: 3
Value: 2
Value: 1
Producer-Consumer Pattern
Data Pipeline
-- Producer coroutine
local function data_producer(data_set)
for _, item in ipairs(data_set) do
print("Producing:", item)
coroutine.yield(item)
end
return "Production complete"
end
-- Consumer function
local function data_consumer(producer_co)
local results = {}
while coroutine.status(producer_co) ~= "dead" do
local success, item = coroutine.resume(producer_co)
if success and item then
if type(item) == "string" and item ~= "Production complete" then
-- Process the item
local processed = string.upper(item) .. "_PROCESSED"
print("Consuming and processing:", item, "->", processed)
table.insert(results, processed)
elseif item == "Production complete" then
print("Consumer received:", item)
break
end
end
end
return results
end
-- Test producer-consumer
local data = {"apple", "banana", "cherry", "date"}
local producer = coroutine.create(function()
return data_producer(data)
end)
print("Starting producer-consumer pipeline:")
local results = data_consumer(producer)
print("\nFinal results:")
for i, result in ipairs(results) do
print(i .. ":", result)
end
Expected Output:
Starting producer-consumer pipeline:
Producing: apple
Consuming and processing: apple -> APPLE_PROCESSED
Producing: banana
Consuming and processing: banana -> BANANA_PROCESSED
Producing: cherry
Consuming and processing: cherry -> CHERRY_PROCESSED
Producing: date
Consuming and processing: date -> DATE_PROCESSED
Consumer received: Production complete
Final results:
1: APPLE_PROCESSED
2: BANANA_PROCESSED
3: CHERRY_PROCESSED
4: DATE_PROCESSED
Multi-stage Pipeline
-- Multi-stage processing pipeline using coroutines
local function create_pipeline(...)
local stages = {...}
return coroutine.wrap(function()
-- Input stage
local input_data = {"1", "2", "3", "4", "5"}
for _, item in ipairs(input_data) do
local current = item
-- Process through each stage
for stage_num, stage_func in ipairs(stages) do
current = stage_func(current, stage_num)
if not current then break end
end
if current then
coroutine.yield(current)
end
end
end)
end
-- Pipeline stages
local function parse_number(value, stage)
local num = tonumber(value)
print("Stage " .. stage .. " (parse):", value, "->", num)
return num
end
local function square_number(value, stage)
local result = value * value
print("Stage " .. stage .. " (square):", value, "->", result)
return result
end
local function format_result(value, stage)
local result = "Result: " .. value
print("Stage " .. stage .. " (format):", value, "->", result)
return result
end
-- Create and run pipeline
print("Multi-stage pipeline processing:")
local pipeline = create_pipeline(parse_number, square_number, format_result)
for result in pipeline do
print("Final output:", result)
print("---")
end
Expected Output:
Multi-stage pipeline processing:
Stage 1 (parse): 1 -> 1
Stage 2 (square): 1 -> 1
Stage 3 (format): 1 -> Result: 1
Final output: Result: 1
---
Stage 1 (parse): 2 -> 2
Stage 2 (square): 2 -> 4
Stage 3 (format): 4 -> Result: 4
Final output: Result: 4
---
Stage 1 (parse): 3 -> 3
Stage 2 (square): 3 -> 9
Stage 3 (format): 9 -> Result: 9
Final output: Result: 9
---
Stage 1 (parse): 4 -> 4
Stage 2 (square): 4 -> 16
Stage 3 (format): 16 -> Result: 16
Final output: Result: 16
---
Stage 1 (parse): 5 -> 5
Stage 2 (square): 5 -> 25
Stage 3 (format): 25 -> Result: 25
Final output: Result: 25
---
Cooperative Task Scheduler
Simple Task Scheduler
-- Simple cooperative task scheduler
local TaskScheduler = {}
TaskScheduler.__index = TaskScheduler
function TaskScheduler.new()
return setmetatable({
tasks = {},
current_time = 0
}, TaskScheduler)
end
function TaskScheduler:add_task(name, task_function)
local co = coroutine.create(task_function)
table.insert(self.tasks, {
name = name,
coroutine = co,
last_run = 0
})
print("Added task:", name)
end
function TaskScheduler:run_cycle()
self.current_time = self.current_time + 1
print("\n--- Scheduler Cycle " .. self.current_time .. " ---")
local active_tasks = {}
for _, task in ipairs(self.tasks) do
if coroutine.status(task.coroutine) ~= "dead" then
print("Running task:", task.name)
local success, result = coroutine.resume(task.coroutine, self.current_time)
if success then
if result then
print(" Task " .. task.name .. " says:", result)
end
table.insert(active_tasks, task)
else
print(" Task " .. task.name .. " error:", result)
end
else
print("Task " .. task.name .. " completed")
end
end
self.tasks = active_tasks
return #active_tasks > 0
end
function TaskScheduler:run_all()
while self:run_cycle() do
-- Continue until no tasks remain
end
print("\nAll tasks completed")
end
-- Example tasks
local function counter_task(max_count)
return function(current_time)
for i = 1, max_count do
coroutine.yield("Count: " .. i .. " at time " .. current_time)
end
return "Counter finished"
end
end
local function timer_task(intervals)
return function(current_time)
for _, interval in ipairs(intervals) do
coroutine.yield("Timer: " .. interval .. " seconds at time " .. current_time)
end
return "Timer finished"
end
end
-- Test scheduler
local scheduler = TaskScheduler.new()
scheduler:add_task("Counter", counter_task(3))
scheduler:add_task("Timer", timer_task({5, 10, 15}))
scheduler:add_task("Quick", function(time)
coroutine.yield("Quick task at time " .. time)
return "Quick task done"
end)
scheduler:run_all()
Expected Output:
Added task: Counter
Added task: Timer
Added task: Quick
--- Scheduler Cycle 1 ---
Running task: Counter
Task Counter says: Count: 1 at time 1
Running task: Timer
Task Timer says: Timer: 5 seconds at time 1
Running task: Quick
Task Quick says: Quick task at time 1
--- Scheduler Cycle 2 ---
Running task: Counter
Task Counter says: Count: 2 at time 2
Running task: Timer
Task Timer says: Timer: 10 seconds at time 2
Running task: Quick
Task Quick says: Quick task done
Task Quick completed
--- Scheduler Cycle 3 ---
Running task: Counter
Task Counter says: Count: 3 at time 3
Running task: Timer
Task Timer says: Timer: 15 seconds at time 3
--- Scheduler Cycle 4 ---
Running task: Counter
Task Counter says: Counter finished
Task Counter completed
Running task: Timer
Task Timer says: Timer finished
Task Timer completed
All tasks completed
Best Practices
- Clear yielding: Yield at logical points in execution
- Error handling: Always check coroutine.resume() success
- Resource management: Clean up resources in coroutines
- Documentation: Document yield points and expected values
- Testing: Test all coroutine states and transitions
Architect Tip: Coroutines are excellent for implementing parsers, state machines, and event-driven systems. Use them when you need to maintain complex state between function calls.
Common Pitfalls
- Forgetting to yield: Infinite loops without yield points
- Status confusion: Not checking coroutine status before resume
- Memory leaks: Keeping references to completed coroutines
- Error propagation: Not handling errors from resumed coroutines
- Complex state: Making coroutine logic too complicated
Checks for Understanding
- What are the possible states of a coroutine?
- What happens if you resume a dead coroutine?
- How do you pass data to a coroutine?
- When should you use coroutines vs regular functions?
Show Answers
- suspended, running, dead, and normal (when calling another coroutine).
- coroutine.resume() returns false and an error message.
- Pass data as arguments to coroutine.resume() after the first call.
- When you need to maintain state between calls, implement generators, or create cooperative multitasking.
Exercises
- Create a lexical analyzer using coroutines to tokenize source code.
- Build an asynchronous HTTP client simulator using cooperative scheduling.
- Implement a game loop with multiple concurrent game objects using coroutines.