Channels and Coroutines
Channels exchange values between concurrent tasks. They can be buffered or unbuffered and can be combined with channel.select to coordinate multiple operations.
This is an API reference. The basic blocks are isolated snippets; the timeout, fan-in, and non-blocking sections are partial patterns whose named channels and callbacks come from the surrounding application. The worker-pool block is a complete in-process example.
The channel and coroutine globals are always available. Channels coordinate coroutines within one Lua process; use process messaging, functions, or queues across process boundaries.
Creating Channels
An unbuffered channel (size 0) requires a sender and receiver to be ready before a transfer completes. A buffered channel allows sends to complete while buffer space is available.
-- Unbuffered: synchronizes sender and receiver
local sync_ch = channel.new()
-- Buffered: queue up to 10 messages
local work_queue = channel.new(10)
| Parameter | Type | Description |
|---|---|---|
size |
integer | Buffer capacity (default: 0 for unbuffered) |
Returns: channel
Sending Values
Sending blocks until a receiver is ready on an unbuffered channel or until buffer space is available on a buffered channel.
-- Send work to a worker pool
local tasks = {"task-a", "task-b"}
local jobs = channel.new(100)
for i, task in ipairs(tasks) do
jobs:send(task) -- Blocks if buffer full
end
jobs:close() -- Signal no more work
| Parameter | Type | Description |
|---|---|---|
value |
any | Value to send |
Returns: boolean
Sending to a closed channel raises an error.
Receiving Values
Receiving blocks until a value is available or the channel is closed.
-- Worker consuming from job queue
while true do
local job, ok = jobs:receive()
if not ok then
break -- Channel closed, no more work
end
process(job)
end
Here, jobs is the application-provided queue and process is its task-processing callback.
Returns: any, boolean
value, true— a value was receivednil, false— the channel is closed and empty
Closing Channels
Closing a channel causes pending senders to receive an error and pending receivers to receive nil, false. Closing an already closed channel is a no-op.
local results = channel.new(10)
-- Producer fills results
for _, item in ipairs(data) do
results:send(process(item))
end
results:close() -- Signal completion
This isolated producer snippet assumes data and the process callback are provided by the application.
Selecting from Multiple Channels
channel.select waits on multiple channel operations at the same time. It can coordinate event sources, timeouts, and non-blocking checks.
local result = channel.select(cases)
| Parameter | Type | Description |
|---|---|---|
cases |
table | Array of select cases |
default |
boolean | If true, returns immediately when no case ready |
Returns: table
- For a channel case:
{channel, value, ok}—channelis the case's channel,valueis the received/sent value,okis false for a closed-channel receive. - For the default branch (when no case is ready and
default = true):{default = true, ok = true}.
Timeout Pattern
Use time.after() to add a timeout to a channel wait.
local time = require("time")
local result_ch = application_response_channel
local timeout, err = time.after("5s")
if err then
return nil, err
end
local r = channel.select {
result_ch:case_receive(),
timeout:case_receive()
}
if r.channel == timeout then
return nil, errors.new({ kind = errors.TIMEOUT, message = "Operation timed out" })
end
return r.value
This partial pattern assumes the entry lists time in modules: and the application supplies application_response_channel. time.after returns one channel on success; invalid or non-positive durations return nil, error.
Fan-in Pattern
Handle values from multiple sources in one loop.
This process-entry pattern uses ambient process, while the application supplies the shutdown signal and the two handler functions.
local events = process.events()
local inbox = process.inbox()
local shutdown = channel.new()
while true do
local r = channel.select {
events:case_receive(),
inbox:case_receive(),
shutdown:case_receive()
}
if r.channel == shutdown then
break
elseif r.channel == events then
handle_event(r.value)
else
handle_message(r.value)
end
end
Non-Blocking Check
Use a default case to check for available data without blocking.
In this isolated pattern, ch and the process callback come from the application.
local r = channel.select {
ch:case_receive(),
default = true
}
if r.default then
-- Nothing available, do something else
elseif not r.ok then
-- The channel is closed
else
process(r.value)
end
Creating Select Cases
Create send and receive cases for channel.select:
-- Send case - completes when channel can accept value
ch:case_send(value)
-- Receive case - completes when value available
ch:case_receive()
Values in the cases table that are not send or receive cases are ignored. Make sure the table contains at least one valid case unless it also has a default branch.
Worker Pool Pattern
local items = {1, 2, 3, 4}
local num_workers = 2
local function process_item(item)
return item * 2
end
local work = channel.new(#items)
local results = channel.new(#items)
-- Spawn workers
for _ = 1, num_workers do
coroutine.spawn(function()
while true do
local item, ok = work:receive()
if not ok then
return
end
results:send(process_item(item))
end
end)
end
-- Feed work
for _, item in ipairs(items) do
work:send(item)
end
work:close()
-- Collect results
local processed = {}
while #processed < #items do
local result, ok = results:receive()
if not ok then break end
table.insert(processed, result)
end
After the loop, processed contains 2, 4, 6, and 8; result order depends on coroutine scheduling. The workers share channels because they are coroutines in the same Lua process.
Errors
| Condition | Kind | Retryable |
|---|---|---|
| Send on closed channel | runtime error | no |
cases argument to select is not a table |
runtime error | no |
See Also
- Process Management - Process spawning and communication
- Message Queue - Queue-based messaging
- Functions - Function invocation