Lua Coroutines

Coroutine Basics

Create, resume, yield
-- Coroutines are cooperative (not preemptive) threads
local co = coroutine.create(function(name)
  print("Hello, " .. name)
  coroutine.yield()                      -- suspend here
  print("Welcome back, " .. name)
  coroutine.yield()
  print("Goodbye, " .. name)
end)

-- Resume advances to next yield
coroutine.resume(co, "Evan")             -- "Hello, Evan"
print(coroutine.status(co))              -- "suspended"
coroutine.resume(co)                     -- "Welcome back, Evan"
coroutine.resume(co)                     -- "Goodbye, Evan"
print(coroutine.status(co))              -- "dead"

Data Exchange

Passing values through yield/resume
local producer = coroutine.create(function()
  local ports = { 22, 80, 443, 8080 }
  for _, port in ipairs(ports) do
    coroutine.yield(port)                -- send port to consumer
  end
end)

-- Consumer
while true do
  local ok, port = coroutine.resume(producer)
  if not ok or port == nil then break end
  print("Scanning port: " .. port)
end

-- Bidirectional communication
local echo = coroutine.create(function(initial)
  local value = initial
  while true do
    value = coroutine.yield("echo: " .. tostring(value))
  end
end)

local _, resp1 = coroutine.resume(echo, "hello")  -- "echo: hello"
local _, resp2 = coroutine.resume(echo, "world")  -- "echo: world"

Coroutine as Iterator

wrap creates a function-style coroutine
-- coroutine.wrap returns a function (no resume needed)
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

for n in fibonacci(100) do
  print(n)                               -- 0, 1, 1, 2, 3, 5, 8, ...
end

-- File line reader as coroutine
local function lines_from(filename)
  return coroutine.wrap(function()
    local f = io.open(filename, "r")
    if not f then return end
    for line in f:lines() do
      coroutine.yield(line)
    end
    f:close()
  end)
end

for line in lines_from("/etc/hosts") do
  if line:match("^%d") then
    print(line)
  end
end

Coroutine States

Status checking
-- States: "suspended", "running", "dead", "normal"
local co = coroutine.create(function()
  -- "running" while executing
  print(coroutine.status(coroutine.running()))
  coroutine.yield()
  -- resumes here
end)

print(coroutine.status(co))              -- "suspended"
coroutine.resume(co)                     -- prints "running"
print(coroutine.status(co))              -- "suspended"
coroutine.resume(co)                     -- finishes
print(coroutine.status(co))              -- "dead"

-- Error handling
local ok, err = coroutine.resume(co)
print(ok, err)    -- false, "cannot resume dead coroutine"

Practical Pipeline

Coroutine-based data pipeline
-- Producer -> Filter -> Consumer pipeline
local function port_scanner(targets)
  return coroutine.wrap(function()
    for _, target in ipairs(targets) do
      -- simulate scan result
      coroutine.yield({
        host = target.host,
        port = target.port,
        open = (target.port == 22 or target.port == 443),
      })
    end
  end)
end

local function filter_open(source)
  return coroutine.wrap(function()
    for result in source do
      if result.open then
        coroutine.yield(result)
      end
    end
  end)
end

local targets = {
  { host = "10.50.1.40", port = 22 },
  { host = "10.50.1.40", port = 80 },
  { host = "10.50.1.40", port = 443 },
}

for result in filter_open(port_scanner(targets)) do
  print(result.host .. ":" .. result.port .. " OPEN")
end