# Concurrency `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 covers how that interacts with Lua coroutines and how to run several pieces of work at once with `task.join`. Plain Lua coroutines behave exactly as in stock Lua. The moment a coroutine needs to wait on something async — a sleep, a request, a query — run it through `task.join` rather than driving it yourself, for the reason in [Self-driven coroutines](#self-driven-coroutines). ```lua -- Three requests that would take ~3s back-to-back finish in ~1s. local a, b, c = task.join( function() return http.get("https://httpbingo.org/delay/1").status end, function() return http.get("https://httpbingo.org/delay/1").status end, function() return http.get("https://httpbingo.org/delay/1").status end ) print(a, b, c) --> 200 200 200 ``` ## 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 gen = coroutine.wrap(squares) print(gen(3), gen(), gen()) --> 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.join` coroutine — suspends until it is 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 keep making progress. ## Running work concurrently ### `task.join(fn1, fn2, ...)` Run each function as its own coroutine, drive them **concurrently** on the runtime, and return each one's first result positionally once all have finished. If a task raises an error, `task.join` re-raises the first one. Because async calls suspend instead of blocking, the tasks overlap — the wall-clock time is the *longest* task, not the sum: ```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 ``` ### Returning results Each task's first return value lands in the matching slot, so several results come back together: ```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 may 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 ``` ## Self-driven coroutines Async stdlib functions suspend correctly only when the runtime is driving the coroutine — at the top level of your script, or inside a `task.join` coroutine. If you drive a coroutine **yourself** with `coroutine.resume` or `coroutine.wrap`, an 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. **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 `task.join` wraps each function in a Lua coroutine and polls them all on the tokio runtime. The concurrency comes from the **reactor** — timers and I/O yielding control while they wait — not from extra threads. - **Concurrency, not parallelism.** Everything runs on one thread; there are simply many operations in flight at once. - **I/O-bound work overlaps.** Sleeps, HTTP requests, and database queries all wait at the same time. - **CPU-bound work does not yield.** A tight compute loop with no async calls blocks its siblings until it finishes or reaches an async call. ## Full example ```lua -- Fetch several resources at once, then combine them. local function get(url) return function() return http.getJSON(url) end end local user, repos = task.join( get("https://api.github.com/users/torvalds"), get("https://api.github.com/users/torvalds/repos") ) print(user.name) print(#repos .. " public repos") -- Both requests ran concurrently: ~1 round-trip of latency, not 2. ``` A runnable version of these patterns ships in [`lua/coroutines-demo.lua`](../lua/coroutines-demo.lua) — run it with `a lua/coroutines-demo.lua`.