ractor-sharing 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data/docs/lockhash.md ADDED
@@ -0,0 +1,150 @@
1
+ # Ractor::LockHash
2
+
3
+ A Hash that Ractors can share. Reads are allowed anywhere; every write goes
4
+ through `synchronize`, and whatever one `synchronize` changes, other Ractors see
5
+ all of it or none of it.
6
+
7
+ ```ruby
8
+ require "ractor/lockhash"
9
+
10
+ board = Ractor::LockHash.new
11
+
12
+ 4.times.map do |i|
13
+ Ractor.new(board, i) do |b, id|
14
+ 100.times {|n| b.synchronize {|x| x[id] = n } }
15
+ end
16
+ end.each(&:join)
17
+
18
+ board.to_h.sort.to_h #=> {0 => 99, 1 => 99, 2 => 99, 3 => 99} # whoever wrote first is first
19
+ ```
20
+
21
+ ## Why not a LockVar holding a Hash
22
+
23
+ You can put a frozen Hash in a [`Ractor::LockVar`](lockvar.md), but then changing
24
+ one entry copies the whole thing:
25
+
26
+ ```ruby
27
+ lv = Ractor::LockVar.new({}.freeze)
28
+ lv.update { it.merge(k: 1).freeze } # O(n) per write
29
+ ```
30
+
31
+ `LockHash` keeps a real Hash and writes into it, so one entry costs one entry.
32
+
33
+ ## API
34
+
35
+ ```ruby
36
+ h = Ractor::LockHash.new(initial = nil)
37
+
38
+ h[key] # read
39
+ h.fetch(key, default) # read, with the usual default / block / KeyError
40
+ h.key?(key)
41
+ h.keys / h.to_h # a frozen, shareable snapshot of the whole hash
42
+ h.inspect
43
+
44
+ h.synchronize {|h| ... } # the only place writes are allowed; yields the LockHash
45
+ h[key] = value # inside synchronize
46
+ h.delete(key) # inside synchronize
47
+ h.clear # inside synchronize
48
+ ```
49
+
50
+ There is no `size` and no `empty?`. A count handed back after the lock is
51
+ released is already stale, and inside a section `keys` says the same thing.
52
+
53
+ `fetch` prefers its block to an explicit default, as `Hash#fetch` does, and runs
54
+ that block **after** the lookup has released the lock, so a default that reads
55
+ this hash again is fine.
56
+
57
+ Keys and values must be **shareable**; `ArgumentError` otherwise. The LockHash
58
+ itself is frozen and shareable, so it can be passed to any Ractor.
59
+
60
+ ### Writes only inside `synchronize`
61
+
62
+ ```ruby
63
+ h = Ractor::LockHash.new
64
+ h[:a] = 1
65
+ # => NoMethodError: '[]=' is only allowed inside Ractor::LockHash#synchronize
66
+ ```
67
+
68
+ It is a `NoMethodError` for the same reason calling a private method is: the
69
+ method is there, but not callable from where you are.
70
+
71
+ Every write being inside a block is what makes reading an entry and writing it
72
+ back one step rather than two:
73
+
74
+ ```ruby
75
+ # WRONG: refused, and it was a lost update anyway
76
+ h[:hits] = h[:hits] + 1
77
+ ```
78
+
79
+ ```ruby
80
+ h = Ractor::LockHash.new(hits: 0)
81
+ h.synchronize {|h| h[:hits] = h[:hits] + 1 } # right
82
+ ```
83
+
84
+ The block is handed the LockHash, **never the Hash inside it**, so no reference
85
+ to the state can escape and be written to later, or from another Ractor.
86
+
87
+ ### The two idioms
88
+
89
+ Almost everything a shared hash gets used for is one of these, and both are one
90
+ `synchronize`:
91
+
92
+ ```ruby
93
+ h = Ractor::LockHash.new
94
+ k = :key
95
+
96
+ h.synchronize {|h| h[k] ||= 42 } # memoize: computed once, by one caller
97
+ h.synchronize {|h| h[:n] = (h[:n] || 0) + 1 } # count: read and write in one step
98
+ ```
99
+
100
+ There is no `compute` or `fetch_or_store` here. One lock covers the whole hash,
101
+ so a dedicated method for one key would run at exactly the same speed as the
102
+ block above, and only spend a name. Such methods start to mean something when a
103
+ lock can be taken per key, and this one is not. See below.
104
+
105
+ ### What one `synchronize` gives you
106
+
107
+ Everything it changes becomes visible together. A reader calling `[]` or `to_h`
108
+ waits for a section in flight rather than looking inside one:
109
+
110
+ ```ruby
111
+ board = Ractor::LockHash.new
112
+ i = 1
113
+ board.synchronize {|b| b[:x] = i; b[:y] = i } # a reader never sees x != y
114
+ ```
115
+
116
+ That is atomicity **across the keys of this hash**. Across separate objects it is
117
+ not: taking a *different* LockHash, or a LockVar, from inside a `synchronize`
118
+ raises `Ractor::NestedLockError`, because that is where lock-order deadlocks come
119
+ from. Several objects that must change together are [`Ractor::TVar`](tvar.md)'s
120
+ job. Nesting `synchronize` on the *same* hash is allowed, so a section may call a
121
+ method that takes one again.
122
+
123
+ There is no rollback either. A block that raises keeps whatever it had already
124
+ written; the lock is released, nothing more.
125
+
126
+ ## The cost of that atomicity
127
+
128
+ One lock covers the whole hash, so **writes to unrelated keys wait for each
129
+ other**, and a snapshot is O(n). A hash written to constantly is better modelled
130
+ as one [`Ractor::LockVar`](lockvar.md) per key, which scales with the cores.
131
+ That is open to you whenever you never need two keys to change together.
132
+
133
+ **Reads take the lock too, so they do not scale either.** Sixteen Ractors reading
134
+ one shared LockHash cost 489 ns per read, against 23 ns when each has a hash of
135
+ its own; a [`Ractor::TVar`](tvar.md) read costs 9 ns whether it is shared or not,
136
+ because it takes no lock. One exclusive lock covers the whole hash, so a reader cannot be let in beside a
137
+ writer the way a TVar's single slot can. If your load is read heavy
138
+ and the hash is shared, that is the number that will decide it.
139
+
140
+ Acquisition is not FIFO: a thread may barge ahead of waiters, so readers hammering
141
+ a hash in a tight loop can starve a writer. Keep sections short, and give busy
142
+ reader loops something else to do between reads.
143
+
144
+ ## Keep the block short
145
+
146
+ The block holds the lock while it runs, so every other reader and writer of this
147
+ hash waits for it. Change the entries and nothing else: no IO, no waiting on
148
+ anything, no calling out to code that might.
149
+
150
+ Part of [ractor-sharing](../README.md).
data/docs/lockvar.md ADDED
@@ -0,0 +1,280 @@
1
+ # Ractor::LockVar
2
+
3
+ **One** variable that Ractors can share. It holds one shareable object; any
4
+ Ractor can read it, and any Ractor can replace what is in it, one at a time.
5
+ Several variables that have to change together are `Ractor::TVar`'s job, and a
6
+ whole hash of them is [`Ractor::LockHash`](lockhash.md)'s.
7
+
8
+ ```ruby
9
+ require "ractor/lockvar"
10
+
11
+ counter = Ractor::LockVar.new(0)
12
+
13
+ rs = 4.times.map do
14
+ Ractor.new(counter) do |c|
15
+ 1000.times { c.update {|n| n + 1 } }
16
+ end
17
+ end
18
+ rs.each(&:join)
19
+
20
+ p counter.value #=> 4000
21
+ ```
22
+
23
+ ## API
24
+
25
+ ```ruby
26
+ lv = Ractor::LockVar.new(initial = nil)
27
+
28
+ lv.value # read, under the lock -- a snapshot, never an input to update
29
+ lv.update {|v| new_v } # replace the value, under the lock; returns new_v
30
+ lv.increment(n = 1) # add n under the lock, as update {|v| v + n } would
31
+ lv.inspect
32
+ ```
33
+
34
+ `inspect` does not take the lock, so it never blocks and never raises: call it
35
+ from a debugger, from inside another update, from anywhere. The value it shows
36
+ may be one update out of date, which for a display is the right trade.
37
+
38
+ A variable, not a lock: there is no lock, unlock, or owner query. `value` and
39
+ `update` are the whole of it, and `increment` is there because a counter is what
40
+ a shared variable most often is.
41
+
42
+ ### The value must be shareable
43
+
44
+ A LockVar holds one **shareable** object, and so does everything you store into
45
+ it. Anything else raises `ArgumentError`:
46
+
47
+ ```ruby
48
+ Ractor::LockVar.new({}) # => ArgumentError: only shareable object are allowed
49
+ lv.update { [1, 2] } # => ArgumentError
50
+ lv.update { [1, 2].freeze } # fine
51
+ lv.update { {a: 1}.freeze } # fine
52
+ ```
53
+
54
+ That is what makes a LockVar safe to hand to any Ractor: it is frozen and
55
+ shareable itself, and the value inside it is too, so nothing reachable through it
56
+ can be mutated behind the lock's back. It also means an update replaces the value
57
+ rather than modifying it: `lv.update { it.merge(k => v).freeze }`, not
58
+ `lv.value[k] = v`.
59
+
60
+ A rejected value leaves the variable as it was.
61
+
62
+ ### Reads and updates
63
+
64
+ Both take the lock, so an update's **whole block** is atomic as far as readers
65
+ are concerned, not just its final store. That is what an update block needs when
66
+ it makes anything else observable:
67
+
68
+ ```ruby
69
+ lv = Ractor::LockVar.new(0)
70
+ lv.update {|v| $last = v; v + 1 } # every reader sees lv.value > $last
71
+ ```
72
+
73
+ Blind assignment is `lv.update { x }` (the block just ignores the old value).
74
+ There is no `value=`: an unlocked write would silently discard a concurrent
75
+ `update` that had already read the old value.
76
+
77
+ Note that the block's result *is* the new value, so a block that forgets to
78
+ return it clears the variable: `lv.update {|v| puts v }` stores `nil`.
79
+
80
+ `increment` is there because adding to a number is the most common update of all;
81
+ it is the block form with the block written for you, and behaves the same way in
82
+ every respect, including refusing to store a sum that is not shareable.
83
+
84
+ ## Read-modify-write belongs inside the block
85
+
86
+ Any new value computed from the current one has to be computed inside `update`,
87
+ from the value the block is given. Reading outside and writing inside is broken:
88
+ another update lands in between, and yours discards it. Counting is only the
89
+ smallest example. The same goes for appending to a frozen array, merging into a
90
+ frozen hash, clamping, toggling, anything that reads before it writes.
91
+
92
+ ```ruby
93
+ # WRONG
94
+ v = lv.value
95
+ lv.update { v + 1 }
96
+
97
+ h = lv.value
98
+ lv.update { h.merge(key => 1).freeze }
99
+ ```
100
+
101
+ ```ruby
102
+ # RIGHT
103
+ lv = Ractor::LockVar.new(0)
104
+ lv.update { it + 1 }
105
+
106
+ h = Ractor::LockVar.new({}.freeze)
107
+ h.update { it.merge(key: 1).freeze }
108
+ ```
109
+
110
+ Four Ractors incrementing 500 times each:
111
+
112
+ ```
113
+ wrong: 829 / 2000 (1171 updates lost)
114
+ right: 2000 / 2000
115
+ ```
116
+
117
+ `value` is for looking: a snapshot, true when it was taken and possibly stale by
118
+ the time you use it. **Never feed it back into `update`.**
119
+
120
+ Nothing in the library can catch this for you, so the warning is the whole
121
+ defence.
122
+
123
+ ## One variable, and how that differs from TVar
124
+
125
+ The unit here is a single variable. That is the whole distinction between this
126
+ and its neighbour, not optimistic versus pessimistic, which is only how each one
127
+ happens to be built.
128
+
129
+ | | `Ractor::LockVar` | [`Ractor::TVar`](tvar.md) |
130
+ |---|---|---|
131
+ | synchronizes | one variable | several variables together |
132
+ | written as | `lv.update {\|v\| ... }` | `Ractor.atomically { ... }` |
133
+ | on conflict | waits its turn | rolls back and runs the block again |
134
+ | the block runs | exactly once | as many times as it takes |
135
+ | so the block may | have side effects | only compute |
136
+ | lock ordering | refused: one variable at a time | not a question |
137
+
138
+ A transaction only starts to mean something once there is a second variable, so
139
+ for one variable there is nothing to express beyond a read-modify-write, which
140
+ is why reading and updating are all there is to it.
141
+
142
+ The row that decides most cases is the rollback. A `TVar` transaction that loses
143
+ a race is discarded and run again, so its block has to be safe to run twice:
144
+ anything it did that was not a `TVar` write has already happened and will happen
145
+ again. A `LockVar` update waits for its turn instead, and then runs once.
146
+
147
+ ```ruby
148
+ $log = []
149
+ lv = Ractor::LockVar.new(0)
150
+ lv.update {|v| $log << v; v + 1 } # logs exactly once
151
+ ```
152
+
153
+ Touching another LockVar from inside an update is refused:
154
+
155
+ ```ruby
156
+ a = Ractor::LockVar.new(1)
157
+ b = Ractor::LockVar.new(2)
158
+ a.update {|v| b.value }
159
+ # => Ractor::NestedLockError:
160
+ # already inside another Ractor::LockVar;
161
+ # use Ractor::TVar to change several of them together
162
+ ```
163
+
164
+ Lock ordering is where locking goes wrong, and refusing the first nesting turns a
165
+ rare production deadlock into a deterministic error. Reaching for a second
166
+ variable is the sign that you wanted a transaction: `Ractor::TVar` logs reads and
167
+ writes and retries on conflict, so it needs no lock order at all.
168
+
169
+ No LockVar can be touched from inside an update, its own included. The block is
170
+ handed the value it needs, and a nested update's write would be discarded by the
171
+ outer block's result anyway. The holder is tracked per **thread**.
172
+
173
+ ## Keep the block short
174
+
175
+ The block holds the lock while it runs, so everything else waiting on this
176
+ variable waits for it. Compute the new value and nothing more: no IO, no waiting
177
+ on anything, no calling out to code that might. This is not a `LockVar`
178
+ restriction so much as the rule for any critical section, and `TVar` wants the
179
+ same thing for its own reason: a transaction is validated against the version it
180
+ read when it started, so a long block is a long window for somebody else to
181
+ invalidate it.
182
+
183
+ ## Performance
184
+
185
+ The workload is one shared value, a frozen `{status:, seq:}` record: a **read**
186
+ takes it out, an **update** puts a new frozen one in its place. Numbers are **ns
187
+ per completed operation across all Ractors**, so one that halves when the Ractors
188
+ double means it scaled. Measured on 16 cores with the CPU governor fixed at
189
+ `performance`, on ruby 4.1.0dev; `benchmark/family.rb` runs the same comparison
190
+ and checks after every run that no update was lost. Each cell is the median of
191
+ three runs, and these cells moved by 0% to 10% between independent sweeps.
192
+
193
+ ### Reading
194
+
195
+ ```ruby
196
+ n.times.map {|i| Ractor.new(vars[i]) {|v| K.times { v.value } } }.each(&:join)
197
+ ```
198
+
199
+ | Ractors | shared `LockVar#value` (ns) | shared `TVar#value` (ns) | own `LockVar#value` (ns) | own `TVar#value` (ns) |
200
+ |---:|---:|---:|---:|---:|
201
+ | 1 | 77 | 74 | 74 | 68 |
202
+ | 2 | 115 | 43 | 47 | 42 |
203
+ | 4 | 297 | 23 | 23 | 32 |
204
+ | 8 | 347 | 17 | 12 | 10 |
205
+ | 16 | 365 | 9 | 10 | 9 |
206
+
207
+ **A shared `LockVar` does not scale for reading, and this is the number to know
208
+ before choosing it.** `#value` takes the lock, so sixteen Ractors reading one
209
+ variable stand in a queue and the read costs 365 ns instead of 10. `TVar#value`
210
+ outside a transaction takes nothing, so it reads the same whether the variable is
211
+ shared or not. Give each Ractor a variable of its own and both scale to the
212
+ machine's limit.
213
+
214
+ What the lock buys is the guarantee in *Reads and updates* above: a reader waits
215
+ for an update in flight, so an update block is atomic to readers and not just its
216
+ final store. If your load is read heavy and shared, that guarantee is expensive.
217
+
218
+ ### Updating
219
+
220
+ ```ruby
221
+ v.update {|rec| { status: rec[:status], seq: rec[:seq] + 1 }.freeze }
222
+ ```
223
+
224
+ | Ractors | shared `LockVar#update` (ns) | shared `TVar` `atomically` (ns) | own `LockVar#update` (ns) | own `TVar` `atomically` (ns) |
225
+ |---:|---:|---:|---:|---:|
226
+ | 1 | 359 | 358 | 352 | 351 |
227
+ | 2 | 823 | 366 | 209 | 204 |
228
+ | 4 | 1014 | 384 | 106 | 132 |
229
+ | 8 | 1017 | 462 | 56 | 99 |
230
+ | 16 | 1102 | 509 | 50 | 108 |
231
+
232
+ **Fought over, neither scales and `TVar` stays about 2× ahead**, because the
233
+ loser of a race retries a short block where `LockVar` parks the thread and wakes
234
+ it through a port, which costs more than the block did; and a transaction that
235
+ keeps losing backs off, a 100 ns spin per consecutive loss, before running
236
+ again. The shared-16 `TVar` cell is the volatile one, landing anywhere from 500
237
+ to 870 ns between sweeps; it is quoted as the median of seven runs.
238
+
239
+ **Spread out, `LockVar` scales and `TVar` does not**: 352 ns down to 50 is 7.0×,
240
+ against 3.3× for `TVar`. Every committing transaction takes one process wide lock
241
+ to allocate the next version number, whichever variable it touched, and that lock
242
+ is the ceiling.
243
+
244
+ **Do not choose `LockVar` for speed on a contended variable.** How much any of
245
+ this matters depends on how often your variable is actually contended and how
246
+ much of your load is reads, which are properties of your program rather than of
247
+ either class, so measure yours. What `LockVar` gives you regardless is that the
248
+ block runs once.
249
+
250
+ ### Not increment
251
+
252
+ `LockVar#increment` and `TVar#increment` each take a fast path that adds two
253
+ Fixnums without running any Ruby, so they are not a measurement of either class:
254
+ 77 ns and 89 ns for one Ractor on its own variable, 338 ns and 144 ns for
255
+ sixteen on one.
256
+
257
+ ## Implementation notes
258
+
259
+ * The lock state is protected by a native mutex that is held for a few
260
+ instructions only and **never across Ruby code**, so a waiter can never keep
261
+ another Ractor from reaching a GC safepoint.
262
+ * A thread that has to wait parks on a `Ractor::Port` of its own rather than on a
263
+ condition variable. `Port#receive` goes through the VM scheduler, so the wait
264
+ **rides the M:N scheduler** (enabled by default on non-main Ractors) and stays
265
+ interruptible: `Thread#kill` on a waiter works and leaves the lock untouched.
266
+ * An uncontended `update` touches the native mutex only: no Port is created and
267
+ no message is sent.
268
+ * `update` and `value` are written in C so that no interrupt can be delivered
269
+ between taking the lock and arming the `ensure` that releases it. (With the
270
+ block form written in Ruby, a `Thread#kill` landing in that window stranded the
271
+ lock forever; it reproduced within a couple of iterations.)
272
+ * Only the first waiter is woken, and waiters stay queued until they wake by
273
+ themselves, so a wakeup lost to an interrupt is retried by the next release; a
274
+ waiter that leaves without taking the lock passes the wakeup on. A waiter whose
275
+ Ractor has ended has a closed port, so the wakeup is skipped and the next
276
+ waiter is tried instead.
277
+ * Acquisition is **not FIFO**: a thread may barge ahead of queued waiters.
278
+ * `inspect` never takes the lock, so it neither blocks nor raises.
279
+
280
+ Part of [ractor-sharing](../README.md).
data/docs/tvar.md ADDED
@@ -0,0 +1,124 @@
1
+ # Ractor::TVar
2
+
3
+ A variable Ractors can share, and the one to reach for first.
4
+ [Software transactional memory](https://en.wikipedia.org/wiki/Software_transactional_memory)
5
+ for Ractors and Threads: read and write as many TVars as you like inside
6
+ `Ractor.atomically`, and everything that block changes takes effect together or
7
+ not at all.
8
+
9
+ A TVar holds any shareable object, not only a number:
10
+
11
+ ```ruby
12
+ require "ractor/tvar"
13
+
14
+ config = Ractor::TVar.new({ mode: :idle }.freeze)
15
+ version = Ractor::TVar.new("v1".freeze)
16
+
17
+ Ractor.atomically do
18
+ config.value = config.value.merge(mode: :running).freeze
19
+ version.value = "v2".freeze # nobody sees v1 running, or v2 idle
20
+ end
21
+ ```
22
+
23
+ One variable is a transaction with one variable in it, and reads the same way:
24
+
25
+ ```ruby
26
+ seen = Ractor::TVar.new([].freeze)
27
+ Ractor.atomically { seen.value = (seen.value + [:x]).freeze }
28
+ ```
29
+
30
+ Where two variables have to agree, that is the whole point:
31
+
32
+ ```ruby
33
+ from = Ractor::TVar.new(100)
34
+ to = Ractor::TVar.new(0)
35
+
36
+ Ractor.atomically do
37
+ from.value -= 10
38
+ to.value += 10 # no one ever sees the money in neither account
39
+ end
40
+ ```
41
+
42
+ Nothing is locked while the block runs. Each transaction reads a consistent
43
+ snapshot and, at the end, commits only if nothing it read has changed since;
44
+ otherwise it is **rolled back and run again**. So a block may run more than once,
45
+ and must be safe to: keep it to reading and writing TVars, with no side effects
46
+ and no waiting.
47
+
48
+ ```ruby
49
+ tv = Ractor::TVar.new(0)
50
+ rs = 4.times.map { Ractor.new(tv) {|t| 10_000.times { Ractor.atomically { t.value += 1 } } } }
51
+ rs.each(&:join)
52
+ tv.value #=> 40000
53
+ ```
54
+
55
+ ## API
56
+
57
+ ```ruby
58
+ tv = Ractor::TVar.new(initial = nil) # any shareable object
59
+
60
+ Ractor.atomically { ... } # everything inside is one transaction
61
+
62
+ tv.value # read; inside a transaction or out
63
+ tv.value = v # write; only inside a transaction
64
+ tv.increment(n = 1) # add in one step; inside or out, for values that answer to +
65
+ ```
66
+
67
+ Values must be shareable; `ArgumentError` otherwise.
68
+
69
+ **A write needs a transaction.** `tv.value = v` on its own raises
70
+ `Ractor::TransactionError`, "can not set without transaction". There is no
71
+ one-off write, because a write on its own is where a lost update comes from:
72
+
73
+ ```ruby
74
+ Ractor.atomically { tv.value = tv.value + 1 } # right
75
+ tv.increment # right, and shorter
76
+ ```
77
+
78
+ A read outside a transaction is allowed, and returns the value as it stands.
79
+ `increment` is allowed too: outside a transaction it does the whole add in one
80
+ step, under the variable's own lock when both sides are Fixnums, and as a one-off
81
+ transaction otherwise.
82
+
83
+ `Ractor::TransactionError` is raised for a transaction that cannot proceed;
84
+ `Ractor::RetryTransaction` is what a rollback is made of.
85
+
86
+ ## When something else fits better
87
+
88
+ * One variable, or a block that must not run twice:
89
+ [`Ractor::LockVar`](lockvar.md), which waits its turn instead of retrying.
90
+ * State you do not want to freeze, a mutable object updated in place:
91
+ [`Ractor::ActorHash`](actor_hash.md) or [`Ractor::ActiveObject`](active_object.md).
92
+
93
+ ## Scaling
94
+
95
+ **Reads outside a transaction cost nothing and scale.** Sixteen Ractors reading
96
+ one shared TVar cost 9 ns per read, the same as sixteen reading their own, where
97
+ the pessimistic [`Ractor::LockVar`](lockvar.md) costs 365 ns for the same thing
98
+ because its read takes the lock. If your load is read heavy and the state is
99
+ shared, this is the reason to be here.
100
+
101
+ A read outside a transaction is a read of one slot. It is not a snapshot across
102
+ several: two TVars that have to agree must be read inside one
103
+ `Ractor.atomically`, the same way they are written.
104
+
105
+ **Commits do not run in parallel.** Every committing transaction takes one
106
+ process wide lock to allocate its version number, whichever variables it touched.
107
+ Sixteen Ractors updating sixteen *unrelated* TVars get about 3.3× the throughput of
108
+ one, where sixteen LockVars get 7.0×. On an update as small as `increment` there
109
+ is no gain left at all: sixteen Ractors on sixteen TVars get no more throughput
110
+ than one does (both about 80 ns, inside the noise), because the commit is then
111
+ all of the work. Transaction
112
+ bodies do run in parallel; it is the commit that does not.
113
+
114
+ **Fought over, retrying beats waiting.** Sixteen Ractors updating the *same*
115
+ variable cost about 509 ns per completed update against 1102 for a LockVar,
116
+ because the loser of a race runs a short block again rather than parking a
117
+ thread and waking it. A transaction that loses twice in a row also **backs
118
+ off**, spinning about 100 ns per consecutive loss before running again, which
119
+ trims the work thrown away in a storm and costs an occasionally contended write
120
+ nothing measurable. That contended figure is the volatile one, landing anywhere
121
+ from 500 to 870 ns between sweeps. The full tables are in
122
+ [the README](../README.md#performance).
123
+
124
+ Part of [ractor-sharing](../README.md).
@@ -0,0 +1,2 @@
1
+ require 'mkmf'
2
+ create_makefile('ractor/lock')