Acquis 9 - Event Loop

This acquis is currently only available on unstable builds and its contents may change at any time.

The event loop enables detached coroutines to perform non-blocking I/O and timed sleeps. Coroutines marked with coroutine.detach participate in event-driven execution, automatically yielding on I/O operations and resuming when ready.

Detaching coroutines

Mark a coroutine for event-driven execution with coroutine.detach:

local co = coroutine.create(function()
  coroutine.sleep(0.1)  -- yields for 100ms
  return "done"
end)

coroutine.detach(co)
local ok, result = coroutine.resume(co)
print(result)  -- "done"

Once detached, the coroutine can use coroutine.sleep and will automatically yield during network I/O that would block.

Sleeping

coroutine.sleep(seconds) yields the coroutine for the specified duration:

coroutine.sleep(1.5)    -- sleep 1.5 seconds
coroutine.sleep(0.001)  -- sleep 1 millisecond
coroutine.sleep(0)      -- yield immediately, resume on next tick

Sleep only works in detached coroutines. Calling it in a regular coroutine raises an error:

local co = coroutine.create(function()
  coroutine.sleep(0.1)  -- error: not detached
end)
local ok, err = coroutine.resume(co)
assert(not ok)

Non-blocking network I/O

Network operations in detached coroutines automatically become non-blocking:

local co = coroutine.create(function()
  -- These operations yield when they would block
  local status, body = network.fetch("https://example.com/api")
  return status
end)

coroutine.detach(co)
local ok, status = coroutine.resume(co)
print(status)  -- 200

The event loop waits for socket readiness using the platform’s native event API (epoll on Linux, kqueue on macOS, IOCP on Windows).

Multiple operations

Detached coroutines can perform multiple I/O operations in sequence:

local co = coroutine.create(function()
  local s1 = network.fetch("https://api.example.com/users")
  coroutine.sleep(0.1)  -- rate limit
  local s2 = network.fetch("https://api.example.com/posts")
  return s1, s2
end)

coroutine.detach(co)
local ok, status1, status2 = coroutine.resume(co)

Error handling

Errors in detached coroutines propagate to the caller:

local co = coroutine.create(function()
  coroutine.sleep(0.1)
  error("something went wrong")
end)

coroutine.detach(co)
local ok, err = catch coroutine.resume(co)
assert(not ok)
print(err)  -- "something went wrong"

Regular yields

Detached coroutines can still use regular coroutine.yield:

local co = coroutine.create(function()
  coroutine.yield("first")
  coroutine.sleep(0.1)
  coroutine.yield("second")
  return "done"
end)

coroutine.detach(co)
print(coroutine.resume(co))  -- true, "first"
print(coroutine.resume(co))  -- true, "second"
print(coroutine.resume(co))  -- true, "done"

Motivation

Lua’s cooperative coroutines are powerful but limited to synchronous execution. Real-world applications need I/O without blocking the entire program.

Traditional approach

Without an event loop, network operations block:

-- This blocks the entire program
local status = network.fetch("https://slow-api.example.com")
-- Can't do anything else while waiting

Event-driven execution

With coroutine.detach, I/O yields control to the event loop:

local co = coroutine.create(function()
  local status = network.fetch("https://slow-api.example.com")
  return status
end)
coroutine.detach(co)
coroutine.resume(co)  -- Internally yields during I/O, resumes when ready

Opt-in design

Regular coroutines work exactly as before. Only explicitly detached coroutines participate in event-driven execution. This preserves backward compatibility and keeps the behavior predictable.