Concurrency#

Lua has no native threads. The language ships coroutines, a cooperative scheduling primitive that lets a function yield control and be resumed later. The runtime advances one coroutine at a time; there is no preemption and no shared-memory race.

For real parallelism the operator leans on the host. LuaJIT plus lua-lanes runs Lua state per OS thread; OpenResty multiplexes coroutines across nginx workers; cqueues is a single-thread event loop on top of coroutines.

/types`. For network I/O that typically drives a coroutine event loop, see Networking.

Coroutines#

A coroutine is a function that can yield to pause itself and return control to whoever called resume. The yielding side hands values back; the resuming side hands values in.

local co = coroutine.create(function(a, b)
  local x = coroutine.yield(a + b)         -- pause, return a+b
  local y = coroutine.yield(x * 2)         -- pause, return x*2
  return y + 1
end)

print(coroutine.resume(co, 1, 2))   --> true  3
print(coroutine.resume(co, 10))     --> true  20
print(coroutine.resume(co, 100))    --> true  101

coroutine.resume returns true, ...yields on yield or true, ...returns on return; false, err on uncaught error inside the coroutine.

Coroutine API#

Function

Effect

coroutine.create(f)

Wraps f in a new coroutine; returns a thread value.

coroutine.resume(co, ...)

Starts or resumes co. Returns true, ... on success or false, err on error.

coroutine.yield(...)

Suspends the current coroutine, returning the values to whoever called resume.

coroutine.wrap(f)

Returns a function that resumes f; raises on error (no boolean wrapper).

coroutine.status(co)

"suspended", "running", "normal", "dead".

coroutine.isyieldable()

Whether the current context can yield.

coroutine.wrap is the friendlier form when it does not need the true / false discipline of resume.

local next_n = coroutine.wrap(function()
  for i = 1, math.huge do coroutine.yield(i) end
end)

print(next_n(), next_n(), next_n())    --> 1  2  3

Coroutines as iterators#

Generic for accepts any function that returns the next value or nil to stop. coroutine.wrap turns a yielding function into exactly that.

local function range(a, b, step)
  step = step or 1
  return coroutine.wrap(function()
    for i = a, b, step do coroutine.yield(i) end
  end)
end

for i in range(1, 5) do print(i) end

Producer / consumer#

Coroutines are a natural fit for producer / consumer pipelines where each stage waits on the previous one.

local function producer()
  for line in io.lines("input.log") do
    coroutine.yield(line)
  end
end

local function filter(prev)
  for line in coroutine.wrap(prev) do
    if line:find("error") then coroutine.yield(line) end
  end
end

for line in coroutine.wrap(function() filter(producer) end) do
  print(line)
end

cqueues#

cqueues is a single-thread event loop on top of coroutines. The operator writes blocking-style code; the loop schedules around I/O.

$ luarocks install cqueues
local cqueues = require("cqueues")
local socket  = require("cqueues.socket")

local loop = cqueues.new()

loop:wrap(function()
  local s = socket.connect("example.com", 80)
  s:write("GET / HTTP/1.0\r\n\r\n")
  for line in s:lines() do print(line) end
end)

loop:wrap(function()
  for i = 1, 3 do
    print("tick", i)
    cqueues.sleep(1)
  end
end)

loop:loop()

cqueues is the default for any single-host async server work in Lua. The HTTP, DNS, and TLS surfaces ship as companion libraries.

OpenResty light threads#

Inside OpenResty (nginx + LuaJIT) use ngx.thread.spawn to fan out coroutines that share an nginx worker. ngx.semaphore and ngx.shared.DICT cover the coordination primitives.

local function fetch(url)
  return ngx.location.capture(url)
end

local t1 = ngx.thread.spawn(fetch, "/api/a")
local t2 = ngx.thread.spawn(fetch, "/api/b")

local _, r1 = ngx.thread.wait(t1)
local _, r2 = ngx.thread.wait(t2)

lua-lanes#

For true preemptive parallelism on multiple cores, lua-lanes spawns a new Lua state per OS thread. State is isolated; data crosses lane boundaries through linda channels.

local lanes = require("lanes").configure()

local function work(n) return n * n end

local h = lanes.gen("*", work)(7)
print(h[1])                  --> 49

Picking the model.

Need

Reach for

cooperative I/O in one process

coroutines + cqueues

long-lived servers behind nginx

OpenResty (ngx.thread, cosockets)

CPU-bound parallel work

lua-lanes (or shell out to a process per core)

References#