# Concurrency: coroutines and `task` `a` runs every script on an async core ([tokio](https://tokio.rs/)). The async stdlib calls — `os.sleep`, the `http` client, `sqlite` queries — don't block the OS thread while they wait; they suspend and let other work run. This page explains how that interacts with Lua coroutines, and how to run several pieces of work concurrently with `task.join`. ## TL;DR - Plain Lua coroutines work exactly as in stock Lua. - Async stdlib calls (`os.sleep`, `http.*`, `sqlite` …) suspend on the runtime — they don't block the thread. - Use **`task.join`** to run multiple coroutines concurrently. N tasks that each wait T seconds finish in ~T, not ~N·T. - **Don't** call async stdlib functions inside a coroutine you drive yourself with `coroutine.resume` / `coroutine.wrap` — see [the gotcha](#the-gotcha). ## Plain coroutines Stock Lua coroutines are pure VM machinery and behave normally: ```lua local function squares(n) for i = 1, n do coroutine.yield(i * i) end end local next = coroutine.wrap(squares) print(next(3), next(3), next(3)) --> 1 4 9 ``` This is ordinary cooperative scheduling: nothing runs concurrently, and the tokio runtime is never involved. ## Async calls suspend, they don't block An async stdlib call at the top level of your script (or inside a `task` coroutine) suspends until it's ready, without tying up the thread: ```lua local t0 = os.microtime() os.sleep(0.10) print(string.format("waited %.3fs", os.microtime() - t0)) --> waited ~0.100s ``` While that sleep is pending, any sibling tasks (see below) keep making progress. ## `task.join` — run work concurrently ``` task.join(fn1, fn2, ...) -> r1, r2, ... ``` Runs each function as its own coroutine, drives them **concurrently** on the runtime, and returns each one's first result positionally once all have finished. If any task raises an error, `task.join` re-raises the first one. Because async calls suspend instead of blocking, the tasks overlap: ```lua local function worker(name, secs) return function() os.sleep(secs) -- suspends; siblings run meanwhile return name end end local t0 = os.microtime() local a, b, c = task.join( worker("slow", 0.30), worker("med", 0.20), worker("fast", 0.10) ) print(a, b, c) --> slow med fast print(string.format("%.3fs", os.microtime() - t0)) --> ~0.300s, not 0.600s ``` The three sleeps run at the same time, so the wall-clock time is the *longest* single task, not the sum. ### Real-world example: concurrent HTTP fetches ```lua local function fetch(url) return function() return http.get(url).status end end local s1, s2, s3 = task.join( fetch("https://httpbingo.org/delay/1"), fetch("https://httpbingo.org/delay/1"), fetch("https://httpbingo.org/delay/1") ) print(s1, s2, s3) --> 200 200 200, in ~1s total instead of ~3s ``` ### Passing results back Each task returns its first value to the corresponding slot: ```lua local me, repos = task.join( function() return http.getJSON("https://api.github.com/users/torvalds") end, function() return http.getJSON("https://api.github.com/users/torvalds/repos") end ) print(me.name, #repos) -- both fetched concurrently ``` ### Nesting `task.join` suspends like any other async call, so a task can itself call `task.join`: ```lua local total = task.join( function() local x, y = task.join( function() os.sleep(0.05); return 21 end, function() os.sleep(0.05); return 21 end ) return x + y -- 42 end, function() os.sleep(0.10); return "sibling" end ) print(total) --> 42 ``` ## The gotcha Async stdlib functions only suspend correctly when the runtime is driving the coroutine — i.e. at the top level of your script, or inside a `task.join` coroutine. If you drive a coroutine **yourself**, the async call does *not* wait: ```lua -- DON'T do this: local co = coroutine.wrap(function() os.sleep(0.50) return "awoke" end) local v = co() -- returns immediately with an opaque value; the 0.5s wait -- never happens, because `coroutine.wrap` can't drive the -- runtime. ``` Under the hood, an async call yields a private marker that only the runtime's scheduler understands; a hand-written `resume`/`wrap` loop just receives that marker and moves on. The rule of thumb: > Use plain `coroutine.*` for pure-Lua generators. The moment a coroutine needs > to `os.sleep`, hit the network, or touch the database, run it through > `task.join` instead. ## How it works (and its limits) `task.join` wraps each function in a Lua coroutine and polls them all concurrently on the tokio runtime. The concurrency comes from the **reactor** — timers and I/O yielding control while they wait — not from extra threads: - Great for **I/O-bound** work: sleeps, HTTP requests, database queries all overlap. - A **CPU-bound** task (a tight compute loop with no async calls) will *not* yield, so it blocks its siblings until it finishes or hits an async call. In other words this is **concurrency, not parallelism**: one thread, many in-flight operations. ## See also - Runnable demo: [`lua/coroutines-demo.lua`](../lua/coroutines-demo.lua) (`a lua/coroutines-demo.lua`)