Channels and Concurrency Primer
This page introduces channels for coordinating coroutines within a process. The examples cover buffering, selection, producer-consumer flows, fan-out, fan-in, and channel closure.
Classification: Reference/API primer. The snippets are independent examples, not a standalone application.
Context and Dependencies
Run these snippets inside an exported function of an executable Lua entry such as
process.lua. The channel and coroutine APIs are ambient globals in that
execution context; they do not need require() calls or modules declarations.
Each snippet creates its own channels and should be evaluated separately.
This page is a primer: each snippet shows one API in isolation. Paste them into the main function of a process.lua entry to run them, as set up in the CLI Applications tutorial.
Creating Channels
Channels pass values between coroutines. Create one with channel.new(capacity):
local ch = channel.new(1) -- buffered channel, capacity 1
Buffered Channels
A send to a buffered channel blocks only when its buffer is full:
local ch = channel.new(3) -- buffer holds 3 items
-- Send without blocking
ch:send(1)
ch:send(2)
ch:send(3)
-- Receive in FIFO order
local v1, ok1 = ch:receive() -- 1, true
local v2, ok2 = ch:receive() -- 2, true
local v3, ok3 = ch:receive() -- 3, true
Unbuffered Channels
Unbuffered channels (capacity 0) synchronize sender and receiver:
local ch = channel.new(0) -- unbuffered
local done = channel.new(1)
coroutine.spawn(function()
ch:send("from spawn") -- blocks until receiver ready
done:send(true)
end)
local val = ch:receive() -- receives "from spawn"
local completed = done:receive()
Channel Select
channel.select waits on multiple channel operations and returns the first one that is ready:
local ch1 = channel.new(1)
local ch2 = channel.new(1)
ch1:send("ch1_value")
local result = channel.select{
ch1:case_receive(),
ch2:case_receive()
}
-- result is a table with: channel, value, ok
result.channel == ch1 -- true
result.value -- "ch1_value"
result.ok -- true
Select with Send
Use case_send to offer a send inside a select. The case is chosen once the channel can accept the value:
local ch = channel.new(1)
local result = channel.select{
ch:case_send("sent"),
default = true
}
if not result.default then
result.ok -- true (send succeeded)
end
local v = ch:receive() -- "sent"
Select blocks until one of its cases is ready. Add default = true to the case table to return immediately instead, with result.default set to true when nothing was ready:
local full = channel.new(1)
full:send("first")
local result = channel.select{
full:case_send("second"),
default = true
}
result.default -- true (buffer full, nothing sent)
Producer-Consumer Pattern
Single producer, single consumer:
local ch = channel.new(5)
local done = channel.new(1)
local consumed = 0
-- Consumer
coroutine.spawn(function()
while true do
local v, ok = ch:receive()
if not ok then break end
consumed = consumed + 1
end
done:send(consumed)
end)
-- Producer
for i = 1, 10 do
ch:send(i)
end
ch:close()
local total = done:receive() -- 10
Ping-Pong Pattern
Synchronize two coroutines:
local ping = channel.new(0)
local pong = channel.new(0)
local rounds_done = channel.new(1)
coroutine.spawn(function()
for i = 1, 5 do
ping:receive()
pong:send("pong")
end
rounds_done:send(true)
end)
for i = 1, 5 do
ping:send("ping")
pong:receive()
end
local completed = rounds_done:receive()
Fan-Out Pattern
One producer, multiple consumers:
local work = channel.new(10)
local results = channel.new(10)
-- Spawn 3 workers
for w = 1, 3 do
coroutine.spawn(function()
while true do
local job, ok = work:receive()
if not ok then break end
results:send(job * 2)
end
end)
end
-- Send work
for i = 1, 6 do
work:send(i)
end
work:close()
-- Collect results
local sum = 0
for i = 1, 6 do
local r = results:receive()
sum = sum + r
end
-- sum = (1+2+3+4+5+6)*2 = 42
Fan-In Pattern
Multiple producers, single consumer:
local output = channel.new(10)
local producer_count = 4
local items_per_producer = 5
-- Spawn producers
for p = 1, producer_count do
local producer_id = p
coroutine.spawn(function()
for i = 1, items_per_producer do
output:send({producer = producer_id, item = i})
end
end)
end
-- Collect all messages
local received = {}
for i = 1, producer_count * items_per_producer do
local msg = output:receive()
table.insert(received, msg)
end
-- Verify all producers sent their items
local counts = {}
for _, msg in ipairs(received) do
counts[msg.producer] = (counts[msg.producer] or 0) + 1
end
Closing Channels
Close channels to signal completion. Receivers get ok = false when channel is closed and empty:
local ch = channel.new(5)
local done = channel.new(1)
coroutine.spawn(function()
local count = 0
while true do
local v, ok = ch:receive()
if not ok then break end -- channel closed
count = count + 1
end
done:send(count)
end)
for i = 1, 10 do
ch:send(i)
end
ch:close() -- signal no more values
local total = done:receive()
Channel Methods
Channel operations:
channel.new(capacity)— Create a channel with the specified buffer sizech:send(value)— Send a value, blocking if the buffer is full; sending to a closed channel raises an errorch:receive()— Receive a value and returnvalue, okch:close()— Close the channel; closing it again raises an errorch:case_send(value)— Create a send case forselectch:case_receive()— Create a receive case forselectchannel.select{cases...}— Wait on multiple operations and returnchannel,value, andokchannel.select{cases..., default = true}— Return{default = true, ok = true}immediately when no case is ready
Next Steps
- Channel Module Reference — Channel API documentation
- Processes — Inter-process communication