You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
175 lines
5.4 KiB
175 lines
5.4 KiB
# 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`.
|
|
|