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`.