Source guide/concurrency.simd
1# Concurrency 2 3Concurrency in Silo is built out of two pieces: an **effect 4hierarchy** that describes where code can run, and the 5**locality modes** from [chapter 17](./17-lifecycle.simd) that 6decide which values can cross a spawn boundary. Everything 7else — tasks, threads, locks, channels — is ordinary library 8code. 9 10Crucially, there are no function colors. Pure words can be 11called from concurrent code; `+Concurrent` code can call into 12synchronous code. The split between async and threads is a 13choice the host makes, not a pervasive property of your code. 14 15## The executor hierarchy 16 17At the top of the concurrency stack sits `+Executor`. Every 18execution unit — an OS thread or an async task — runs under 19some executor. Below that are two children: 20 21- `+Thread` — `+Executor` specialised for OS threads. Its 22 associated `id` type is `ThreadId`. 23- `+Concurrent` — `+Executor` specialised for async tasks. Its 24 associated `id` type is `TaskId`. Adds async-only 25 primitives like cancellation and timeout. 26 27Both children inherit the shared surface — `.spawn`, `.await`, 28`.join-all`, `.yield`, `.sleep`, `.suspend`/`.resume`, 29`.current-id` — so code that constrains only `+Executor` works 30under either. Use the child when you need something specific 31(cancellation only makes sense for async tasks, stack sizes 32only make sense for threads). 33 34The host decides which executor is available. A CLI program 35might install a threadpool that exposes `+Thread`; a 36server might install an async runtime that exposes 37`+Concurrent`. Your code picks the effect it constrains 38against. 39 40## Spawning 41 42`.spawn` takes a quotation and returns a `(Handle val)`: 43 44```silo 45:fn parallel-work ( -> ) +Concurrent 46 [ heavy-computation-1 ] .spawn pop-> h1 47 [ heavy-computation-2 ] .spawn pop-> h2 48 h1 .await 49 h2 .await 50:end 51``` 52 53`.spawn` starts the quotation running on the executor. The 54returned `Handle` is a value you hold until you want the 55result — `.await` consumes the handle and blocks the current 56task until the spawned one finishes. 57 58For batches, `.join-all` takes a `Vec` of handles and waits on 59all of them: 60 61```silo 62urls [ [ pop-> u u http-get ] .spawn ] .map .join-all 63``` 64 65Handles carry the `local` mode (from 66[chapter 17](./17-lifecycle.simd)) so they can't be stored in 67longer-lived containers without care. If a handle's RC drops 68to zero without an `.await` or `.cancel`, the task is cancelled 69automatically — structured concurrency falls out of locality 70and `:dispose`. 71 72## Thread-safety via locality 73 74Here's the important bit. Silo has no `Send`/`Sync` traits and 75no function-coloring. Thread safety is enforced by the 76**locality modes** from the lifecycle chapter: 77 78- A **`local`** value cannot cross a `.spawn` boundary. The 79 compiler rejects the capture. This covers the "this handle 80 shouldn't leave my region" case. 81- A **`unique`** value cannot be aliased, so the question of 82 "is it safe to share across threads?" doesn't come up — you 83 can pass it, but you can't keep a reference on the caller's 84 side. 85- A **`linear`** value must be consumed; if it crosses into a 86 spawned task, the caller has given it up entirely. 87- A **`global`** value can be captured freely. The compiler 88 handles the reference counting so both sides see a 89 consistent view. 90 91No data races because the compiler rejects any capture that 92would create one. 93 94```silo 95:fn use-guard ( (RwLock Int) -> Int ) +Concurrent 96 .read-lock pop-> guard # guard is local 97 [ guard .get ] .spawn .await # compile error — local can't cross spawn 98:end 99``` 100 101## Synchronisation primitives 102 103The standard library ships the usual suspects: 104 105- **`Mutex`** — mutual exclusion; `.lock` returns a guard that 106 disposes to unlock. 107- **`RwLock`** — multi-reader / single-writer; `.read-lock` 108 and `.write-lock` return differently-typed guards. 109- **`Atomic`** — atomic integer / pointer cells for lock-free 110 patterns. 111- **`Sender` / `Receiver`** — **bounded MPSC** channels. 112 `channel` takes a capacity and returns a `(Sender t)` + 113 `(Receiver t)` pair. `Sender` is cloneable (multiple 114 producers); `Receiver` is affine (single consumer — moving 115 one would require giving up your own, no aliases possible). 116 A channel closes when every sender has been dropped or 117 `.close`d, at which point `.recv` returns `None` once the 118 buffer drains. `.select` takes a `(Seq (Receiver t))` and 119 waits on whichever receiver is ready first. 120 121Each uses the same locality-mode story to stay sound. A 122`RwLock`'s read guard is `local` so it can't escape the 123acquired scope; a `Mutex`'s write guard is `unique` so two 124aliased references to it can't exist. 125 126## Async-only primitives 127 128On top of the shared `+Executor` surface, `+Concurrent` adds 129two async-only operations: 130 131- **`.cancel`** — request cancellation of a handle. The task 132 may not stop immediately; the host makes a best effort. 133- **`.timeout`** — run a quotation with a duration limit. 134 Returns `Ok(val)` if the quotation finishes in time or 135 `Err(TimedOut)` if the limit elapses first. 136 137```silo 138:fn fetch-with-timeout ( Url UDuration -> (Result HttpResp Str) ) +Concurrent +Http 139 pop-> limit pop-> url 140 limit [ url http-get .unwrap ] .timeout 141 :match 142 | Ok resp => resp Ok 143 | Err TimedOut => "request timed out" Err 144 :end 145:end 146``` 147 148## Key points 149 150- `+Executor` is the shared surface for concurrency; `+Thread` 151 and `+Concurrent` are the two specialised child effects for 152 OS threads and async tasks respectively. 153- No function colors. Pure code and concurrent code call each 154 other freely; the effect row tells you which is which. 155- `.spawn` returns a `(Handle val)` with `local` mode. 156 `.await` consumes it; dropping it without `.await` cancels 157 the task (structured concurrency via 158 [dispose](./17-lifecycle.simd)). 159- Thread safety is enforced by locality modes from 160 [chapter 17](./17-lifecycle.simd). The compiler rejects 161 spawn-captures that would create data races — no `Send`/ 162 `Sync` traits needed. 163- Standard primitives: `Mutex`, `RwLock`, `Atomic`, channels 164 via `Sender`/`Receiver` with `.select`. 165- Async-only: `.cancel`, `.timeout` on `+Concurrent`. 166 167Next: [variadic functions and reflection](./19-variadic-and-reflection.simd) 168— the `/N` arity suffix, output variadics, and `:type-info`.