ractor-sharing 0.2.0 → 0.3.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.
- checksums.yaml +4 -4
- data/README.md +204 -36
- data/docs/keylockhash.md +141 -0
- data/docs/lockhash.md +39 -15
- data/docs/lockvar.md +26 -23
- data/docs/tvar.md +53 -10
- data/examples/01_bank_transfer.rb +43 -0
- data/examples/02_seat_booking.rb +40 -0
- data/examples/03_feature_flags.rb +30 -0
- data/examples/04_progress.rb +33 -0
- data/examples/05_exactly_once.rb +38 -0
- data/examples/06_metrics_board.rb +47 -0
- data/examples/07_word_count.rb +36 -0
- data/examples/08_lru_cache.rb +59 -0
- data/examples/09_audit_log.rb +31 -0
- data/examples/10_price_quotes.rb +36 -0
- data/examples/11_webshop.rb +104 -0
- data/examples/12_kvstore_wal.rb +84 -0
- data/examples/13_buffered_logger.rb +85 -0
- data/examples/14_api_gateway.rb +180 -0
- data/examples/15_cache_backend.rb +53 -0
- data/examples/16_session_store.rb +74 -0
- data/examples/README.md +35 -0
- data/ext/ractor/lock/keylockhash.c +445 -0
- data/ext/ractor/lock/lock.c +35 -0
- data/ext/ractor/lock/lock.h +10 -0
- data/ext/ractor/lock/lockhash.c +4 -2
- data/ext/ractor/lock/lockvar.c +4 -25
- data/ext/ractor/tvar/tvar.c +17 -13
- data/lib/ractor/keylockhash.rb +3 -0
- data/lib/ractor/sharing/version.rb +1 -1
- data/lib/ractor/sharing.rb +3 -0
- metadata +21 -1
|
@@ -0,0 +1,445 @@
|
|
|
1
|
+
#include "lock.h"
|
|
2
|
+
#include "ruby/st.h"
|
|
3
|
+
|
|
4
|
+
/* Ractor::KeyLockHash - a hash with one lock per key, near enough.
|
|
5
|
+
*
|
|
6
|
+
* Ractor::LockHash is the table lock: one lock over the whole hash, so a
|
|
7
|
+
* section is atomic across its keys and unrelated keys wait for each other.
|
|
8
|
+
* This is the row lock: an update touches one key under one lock, and updates
|
|
9
|
+
* to unrelated keys run in parallel. Nothing here is atomic across two keys;
|
|
10
|
+
* that is LockHash's job (or Ractor::TVar's).
|
|
11
|
+
*
|
|
12
|
+
* "Near enough": the implementation is sharded, the textbook name is lock
|
|
13
|
+
* striping. Keys hash onto a fixed number of shards, each a table with a lock
|
|
14
|
+
* of its own, so two keys sometimes share a lock. That costs a collision now
|
|
15
|
+
* and then and buys freedom from per-entry lock lifetime problems.
|
|
16
|
+
*
|
|
17
|
+
* A plain write is []=, with no ceremony: one key's write is atomic on its
|
|
18
|
+
* own. #update exists for one reason only, computing the new value from the
|
|
19
|
+
* old one, read and stored under the same hold of that key's lock; its block
|
|
20
|
+
* runs exactly once. The claim idiom falls out: whether the block saw nil
|
|
21
|
+
* tells you whether you won.
|
|
22
|
+
*
|
|
23
|
+
* mine = false
|
|
24
|
+
* m.update(id) {|v| v ? v : (mine = true; :claimed) }
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
#define KLH_NSHARDS 64
|
|
28
|
+
|
|
29
|
+
static VALUE rb_cRactorKeyLockHash;
|
|
30
|
+
static ID id_plus;
|
|
31
|
+
|
|
32
|
+
struct klh_shard {
|
|
33
|
+
st_table *tbl; /* shareable VALUE => shareable VALUE */
|
|
34
|
+
struct rs_lock lock;
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
struct keylockhash {
|
|
38
|
+
struct klh_shard shards[KLH_NSHARDS];
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
static st_index_t
|
|
42
|
+
klh_key_hash(st_data_t key)
|
|
43
|
+
{
|
|
44
|
+
return (st_index_t)NUM2LONG(rb_hash((VALUE)key));
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
static int
|
|
48
|
+
klh_key_cmp(st_data_t a, st_data_t b)
|
|
49
|
+
{
|
|
50
|
+
return rb_eql((VALUE)a, (VALUE)b) ? 0 : 1;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
static const struct st_hash_type klh_type = { klh_key_cmp, klh_key_hash };
|
|
54
|
+
|
|
55
|
+
static int
|
|
56
|
+
klh_mark_i(st_data_t key, st_data_t value, st_data_t arg)
|
|
57
|
+
{
|
|
58
|
+
rb_gc_mark((VALUE)key);
|
|
59
|
+
rb_gc_mark((VALUE)value);
|
|
60
|
+
return ST_CONTINUE;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
static void
|
|
64
|
+
klh_mark(void *ptr)
|
|
65
|
+
{
|
|
66
|
+
struct keylockhash *kh = ptr;
|
|
67
|
+
for (int i = 0; i < KLH_NSHARDS; i++) {
|
|
68
|
+
st_foreach(kh->shards[i].tbl, klh_mark_i, 0);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
static void
|
|
73
|
+
klh_free(void *ptr)
|
|
74
|
+
{
|
|
75
|
+
struct keylockhash *kh = ptr;
|
|
76
|
+
for (int i = 0; i < KLH_NSHARDS; i++) {
|
|
77
|
+
st_free_table(kh->shards[i].tbl);
|
|
78
|
+
rs_lock_destroy(&kh->shards[i].lock);
|
|
79
|
+
}
|
|
80
|
+
ruby_xfree(kh);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
static size_t
|
|
84
|
+
klh_memsize(const void *ptr)
|
|
85
|
+
{
|
|
86
|
+
const struct keylockhash *kh = ptr;
|
|
87
|
+
size_t n = sizeof(struct keylockhash);
|
|
88
|
+
for (int i = 0; i < KLH_NSHARDS; i++) n += st_memsize(kh->shards[i].tbl);
|
|
89
|
+
return n;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
static const rb_data_type_t klh_data_type = {
|
|
93
|
+
"Ractor::KeyLockHash",
|
|
94
|
+
{klh_mark, klh_free, klh_memsize, NULL},
|
|
95
|
+
0, 0, RUBY_TYPED_FREE_IMMEDIATELY | RUBY_TYPED_FROZEN_SHAREABLE
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
static struct keylockhash *
|
|
99
|
+
klh_ptr(VALUE self)
|
|
100
|
+
{
|
|
101
|
+
struct keylockhash *kh;
|
|
102
|
+
TypedData_Get_Struct(self, struct keylockhash, &klh_data_type, kh);
|
|
103
|
+
return kh;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
static void
|
|
107
|
+
klh_check_shareable(VALUE val)
|
|
108
|
+
{
|
|
109
|
+
if (RB_UNLIKELY(!rb_ractor_shareable_p(val))) {
|
|
110
|
+
rb_raise(rb_eArgError, "only shareable object are allowed");
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/* The key's #hash runs here, outside every lock. */
|
|
115
|
+
static struct klh_shard *
|
|
116
|
+
klh_shard_for(VALUE self, VALUE key)
|
|
117
|
+
{
|
|
118
|
+
unsigned long h = (unsigned long)NUM2LONG(rb_hash(key));
|
|
119
|
+
return &klh_ptr(self)->shards[h % KLH_NSHARDS];
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
static VALUE
|
|
123
|
+
klh_alloc(VALUE klass)
|
|
124
|
+
{
|
|
125
|
+
struct keylockhash *kh;
|
|
126
|
+
VALUE obj = TypedData_Make_Struct(klass, struct keylockhash, &klh_data_type, kh);
|
|
127
|
+
for (int i = 0; i < KLH_NSHARDS; i++) {
|
|
128
|
+
kh->shards[i].tbl = st_init_table(&klh_type);
|
|
129
|
+
rs_lock_init(&kh->shards[i].lock);
|
|
130
|
+
}
|
|
131
|
+
return obj;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
static int
|
|
135
|
+
klh_copy_pair(VALUE key, VALUE value, VALUE selfv)
|
|
136
|
+
{
|
|
137
|
+
key = rs_hash_key(key);
|
|
138
|
+
klh_check_shareable(key);
|
|
139
|
+
value = rs_shareable_value(value);
|
|
140
|
+
/* single-threaded: nobody else has seen self yet */
|
|
141
|
+
st_insert(klh_shard_for((VALUE)selfv, key)->tbl, (st_data_t)key, (st_data_t)value);
|
|
142
|
+
return ST_CONTINUE;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
static VALUE
|
|
146
|
+
klh_initialize(int argc, VALUE *argv, VALUE self)
|
|
147
|
+
{
|
|
148
|
+
VALUE init = Qnil;
|
|
149
|
+
rb_check_frozen(self); /* send(:initialize) again would write past the locks */
|
|
150
|
+
rb_scan_args(argc, argv, "01", &init);
|
|
151
|
+
if (!NIL_P(init)) {
|
|
152
|
+
Check_Type(init, T_HASH);
|
|
153
|
+
rb_hash_foreach(init, klh_copy_pair, self);
|
|
154
|
+
}
|
|
155
|
+
rb_obj_freeze(self);
|
|
156
|
+
rb_ractor_make_shareable(self);
|
|
157
|
+
return self;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/* --- guarded bodies ------------------------------------------------------- */
|
|
161
|
+
|
|
162
|
+
struct klh_op {
|
|
163
|
+
struct klh_shard *shard;
|
|
164
|
+
VALUE key;
|
|
165
|
+
VALUE value;
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
static VALUE
|
|
169
|
+
klh_lookup_body(VALUE ptr)
|
|
170
|
+
{
|
|
171
|
+
struct rs_guard_arg *arg = (struct rs_guard_arg *)ptr;
|
|
172
|
+
struct klh_op *op = arg->data;
|
|
173
|
+
st_data_t found;
|
|
174
|
+
|
|
175
|
+
if (st_lookup(op->shard->tbl, (st_data_t)op->key, &found)) return (VALUE)found;
|
|
176
|
+
return Qundef;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
static VALUE
|
|
180
|
+
klh_update_body(VALUE ptr)
|
|
181
|
+
{
|
|
182
|
+
struct rs_guard_arg *arg = (struct rs_guard_arg *)ptr;
|
|
183
|
+
struct klh_op *op = arg->data;
|
|
184
|
+
st_data_t found;
|
|
185
|
+
VALUE old = Qnil, next;
|
|
186
|
+
|
|
187
|
+
if (st_lookup(op->shard->tbl, (st_data_t)op->key, &found)) old = (VALUE)found;
|
|
188
|
+
next = rs_shareable_value(rb_yield(old));
|
|
189
|
+
st_insert(op->shard->tbl, (st_data_t)op->key, (st_data_t)next);
|
|
190
|
+
return next;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/* (old or 0) + amount, stored under the key's lock. A missing key counts as
|
|
194
|
+
* zero, the way ActorHash#increment counts it: the tally shape needs no
|
|
195
|
+
* seeding. Fixnums that stay Fixnums skip the dispatch. */
|
|
196
|
+
static VALUE
|
|
197
|
+
klh_increment_body(VALUE ptr)
|
|
198
|
+
{
|
|
199
|
+
struct rs_guard_arg *arg = (struct rs_guard_arg *)ptr;
|
|
200
|
+
struct klh_op *op = arg->data;
|
|
201
|
+
st_data_t found;
|
|
202
|
+
VALUE old = INT2FIX(0), next;
|
|
203
|
+
|
|
204
|
+
if (st_lookup(op->shard->tbl, (st_data_t)op->key, &found)) old = (VALUE)found;
|
|
205
|
+
next = rs_fixnum_add(old, op->value);
|
|
206
|
+
if (RB_UNDEF_P(next)) {
|
|
207
|
+
next = rs_shareable_value(rb_funcall(old, id_plus, 1, op->value));
|
|
208
|
+
}
|
|
209
|
+
st_insert(op->shard->tbl, (st_data_t)op->key, (st_data_t)next);
|
|
210
|
+
return next;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
static VALUE
|
|
214
|
+
klh_aset_body(VALUE ptr)
|
|
215
|
+
{
|
|
216
|
+
struct rs_guard_arg *arg = (struct rs_guard_arg *)ptr;
|
|
217
|
+
struct klh_op *op = arg->data;
|
|
218
|
+
|
|
219
|
+
st_insert(op->shard->tbl, (st_data_t)op->key, (st_data_t)op->value);
|
|
220
|
+
return op->value;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
static VALUE
|
|
224
|
+
klh_delete_body(VALUE ptr)
|
|
225
|
+
{
|
|
226
|
+
struct rs_guard_arg *arg = (struct rs_guard_arg *)ptr;
|
|
227
|
+
struct klh_op *op = arg->data;
|
|
228
|
+
st_data_t key = (st_data_t)op->key, old;
|
|
229
|
+
|
|
230
|
+
if (st_delete(op->shard->tbl, &key, &old)) return (VALUE)old;
|
|
231
|
+
return Qnil;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
static int
|
|
235
|
+
klh_collect_key(st_data_t key, st_data_t value, st_data_t ary)
|
|
236
|
+
{
|
|
237
|
+
rb_ary_push((VALUE)ary, (VALUE)key);
|
|
238
|
+
return ST_CONTINUE;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
static int
|
|
242
|
+
klh_collect_pair(st_data_t key, st_data_t value, st_data_t hash)
|
|
243
|
+
{
|
|
244
|
+
rb_hash_aset((VALUE)hash, (VALUE)key, (VALUE)value);
|
|
245
|
+
return ST_CONTINUE;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
static VALUE
|
|
249
|
+
klh_collect_body(VALUE ptr)
|
|
250
|
+
{
|
|
251
|
+
struct rs_guard_arg *arg = (struct rs_guard_arg *)ptr;
|
|
252
|
+
struct klh_op *op = arg->data;
|
|
253
|
+
|
|
254
|
+
st_foreach(op->shard->tbl, RB_TYPE_P(op->value, T_ARRAY) ? klh_collect_key : klh_collect_pair,
|
|
255
|
+
(st_data_t)op->value);
|
|
256
|
+
return Qnil;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/* One key's #hash or #eql? reaching back into this map raises NestedLockError
|
|
260
|
+
* rather than deadlocking: the inner call may want a shard this thread does
|
|
261
|
+
* not hold, so unlike LockHash there is no safe reentrant path. */
|
|
262
|
+
#define KLH_GUARDED(self, op, body) \
|
|
263
|
+
rs_guarded((self), &(op)->shard->lock, (body), (op), false, \
|
|
264
|
+
"one key is already in hand; two keys together is Ractor::LockHash's job")
|
|
265
|
+
|
|
266
|
+
/* --- methods -------------------------------------------------------------- */
|
|
267
|
+
|
|
268
|
+
/*
|
|
269
|
+
* call-seq:
|
|
270
|
+
* keylockhash[key] -> value or nil
|
|
271
|
+
*
|
|
272
|
+
* Reads one key under that key's shard lock. Unrelated keys do not wait.
|
|
273
|
+
*/
|
|
274
|
+
static VALUE
|
|
275
|
+
klh_aref(VALUE self, VALUE key)
|
|
276
|
+
{
|
|
277
|
+
struct klh_op op = { klh_shard_for(self, key), key, Qnil };
|
|
278
|
+
VALUE found = KLH_GUARDED(self, &op, klh_lookup_body);
|
|
279
|
+
return RB_UNDEF_P(found) ? Qnil : found;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
/*
|
|
283
|
+
* call-seq:
|
|
284
|
+
* keylockhash.fetch(key) -> value or KeyError
|
|
285
|
+
* keylockhash.fetch(key, default) -> value or default
|
|
286
|
+
* keylockhash.fetch(key) {|k| ... } -> value or block result
|
|
287
|
+
*
|
|
288
|
+
* The default and the block run with the lock released.
|
|
289
|
+
*/
|
|
290
|
+
static VALUE
|
|
291
|
+
klh_fetch(int argc, VALUE *argv, VALUE self)
|
|
292
|
+
{
|
|
293
|
+
VALUE key, def, found;
|
|
294
|
+
struct klh_op op;
|
|
295
|
+
|
|
296
|
+
rb_check_arity(argc, 1, 2);
|
|
297
|
+
key = argv[0];
|
|
298
|
+
def = argc > 1 ? argv[1] : Qundef;
|
|
299
|
+
|
|
300
|
+
op.shard = klh_shard_for(self, key);
|
|
301
|
+
op.key = key;
|
|
302
|
+
op.value = Qnil;
|
|
303
|
+
found = KLH_GUARDED(self, &op, klh_lookup_body);
|
|
304
|
+
if (!RB_UNDEF_P(found)) return found;
|
|
305
|
+
if (rb_block_given_p()) return rb_yield(key);
|
|
306
|
+
if (!RB_UNDEF_P(def)) return def;
|
|
307
|
+
rb_raise(rb_eKeyError, "key not found: %+"PRIsVALUE, key);
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
static VALUE
|
|
311
|
+
klh_key_p(VALUE self, VALUE key)
|
|
312
|
+
{
|
|
313
|
+
struct klh_op op = { klh_shard_for(self, key), key, Qnil };
|
|
314
|
+
return RB_UNDEF_P(KLH_GUARDED(self, &op, klh_lookup_body)) ? Qfalse : Qtrue;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
/*
|
|
318
|
+
* call-seq:
|
|
319
|
+
* keylockhash.update(key) {|value_or_nil| new_value } -> new_value
|
|
320
|
+
*
|
|
321
|
+
* Reads, yields and stores under that key's lock: an atomic
|
|
322
|
+
* read-modify-write for one key, and the block runs exactly once. Yields
|
|
323
|
+
* nil for a missing key, so "whether the block saw nil" is "whether you
|
|
324
|
+
* created it" -- the put-if-absent idiom needs nothing more.
|
|
325
|
+
*/
|
|
326
|
+
static VALUE
|
|
327
|
+
klh_update(VALUE self, VALUE key)
|
|
328
|
+
{
|
|
329
|
+
struct klh_op op;
|
|
330
|
+
rb_need_block();
|
|
331
|
+
key = rs_hash_key(key); /* may insert it */
|
|
332
|
+
klh_check_shareable(key);
|
|
333
|
+
op = (struct klh_op){ klh_shard_for(self, key), key, Qnil };
|
|
334
|
+
return KLH_GUARDED(self, &op, klh_update_body);
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
/*
|
|
338
|
+
* call-seq:
|
|
339
|
+
* keylockhash.increment(key, by = 1) -> new value
|
|
340
|
+
*
|
|
341
|
+
* Adds +by+ under that key's lock; a missing key counts as zero. The same
|
|
342
|
+
* as <code>update(key) {|v| (v || 0) + by }</code>.
|
|
343
|
+
*/
|
|
344
|
+
static VALUE
|
|
345
|
+
klh_increment(int argc, VALUE *argv, VALUE self)
|
|
346
|
+
{
|
|
347
|
+
VALUE key, by;
|
|
348
|
+
struct klh_op op;
|
|
349
|
+
|
|
350
|
+
rb_check_arity(argc, 1, 2);
|
|
351
|
+
key = rs_hash_key(argv[0]);
|
|
352
|
+
klh_check_shareable(key); /* may insert it */
|
|
353
|
+
by = argc < 2 ? INT2FIX(1) : argv[1];
|
|
354
|
+
op = (struct klh_op){ klh_shard_for(self, key), key, by };
|
|
355
|
+
return KLH_GUARDED(self, &op, klh_increment_body);
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
/*
|
|
359
|
+
* call-seq:
|
|
360
|
+
* keylockhash[key] = value -> value
|
|
361
|
+
*
|
|
362
|
+
* Stores under that key's lock. No ceremony: a plain write is atomic on its
|
|
363
|
+
* own here, because nothing in this class is atomic across two keys anyway.
|
|
364
|
+
* Reach for #update only when the new value is computed from the old one.
|
|
365
|
+
*/
|
|
366
|
+
static VALUE
|
|
367
|
+
klh_aset(VALUE self, VALUE key, VALUE value)
|
|
368
|
+
{
|
|
369
|
+
struct klh_op op;
|
|
370
|
+
|
|
371
|
+
key = rs_hash_key(key);
|
|
372
|
+
klh_check_shareable(key);
|
|
373
|
+
value = rs_shareable_value(value);
|
|
374
|
+
op = (struct klh_op){ klh_shard_for(self, key), key, value };
|
|
375
|
+
return KLH_GUARDED(self, &op, klh_aset_body);
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
/*
|
|
379
|
+
* call-seq:
|
|
380
|
+
* keylockhash.delete(key) -> old value or nil
|
|
381
|
+
*/
|
|
382
|
+
static VALUE
|
|
383
|
+
klh_delete(VALUE self, VALUE key)
|
|
384
|
+
{
|
|
385
|
+
struct klh_op op = { klh_shard_for(self, key), key, Qnil };
|
|
386
|
+
return KLH_GUARDED(self, &op, klh_delete_body);
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
/* keys and to_h visit the shards one at a time, each under its own lock: a
|
|
390
|
+
* plain mutable copy, consistent per shard but NOT a snapshot of the whole
|
|
391
|
+
* map at one moment. A cross-key snapshot is LockHash territory. */
|
|
392
|
+
static VALUE
|
|
393
|
+
klh_keys(VALUE self)
|
|
394
|
+
{
|
|
395
|
+
VALUE ary = rb_ary_new();
|
|
396
|
+
struct keylockhash *kh = klh_ptr(self);
|
|
397
|
+
|
|
398
|
+
for (int i = 0; i < KLH_NSHARDS; i++) {
|
|
399
|
+
struct klh_op op = { &kh->shards[i], Qnil, ary };
|
|
400
|
+
KLH_GUARDED(self, &op, klh_collect_body);
|
|
401
|
+
}
|
|
402
|
+
return ary;
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
static VALUE
|
|
406
|
+
klh_to_h(VALUE self)
|
|
407
|
+
{
|
|
408
|
+
VALUE hash = rb_hash_new();
|
|
409
|
+
struct keylockhash *kh = klh_ptr(self);
|
|
410
|
+
|
|
411
|
+
for (int i = 0; i < KLH_NSHARDS; i++) {
|
|
412
|
+
struct klh_op op = { &kh->shards[i], Qnil, hash };
|
|
413
|
+
KLH_GUARDED(self, &op, klh_collect_body);
|
|
414
|
+
}
|
|
415
|
+
return hash;
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
static VALUE
|
|
419
|
+
klh_inspect(VALUE self)
|
|
420
|
+
{
|
|
421
|
+
if (!NIL_P(rs_held(rb_thread_current()))) {
|
|
422
|
+
return rb_sprintf("#<%"PRIsVALUE" ...>", rb_obj_class(self));
|
|
423
|
+
}
|
|
424
|
+
return rb_sprintf("#<%"PRIsVALUE" %+"PRIsVALUE">", rb_obj_class(self), klh_to_h(self));
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
void
|
|
428
|
+
Init_keylockhash_class(void)
|
|
429
|
+
{
|
|
430
|
+
id_plus = rb_intern("+");
|
|
431
|
+
rb_cRactorKeyLockHash = rb_define_class_under(rb_cRactor, "KeyLockHash", rb_cObject);
|
|
432
|
+
rb_define_alloc_func(rb_cRactorKeyLockHash, klh_alloc);
|
|
433
|
+
rb_define_method(rb_cRactorKeyLockHash, "initialize", klh_initialize, -1);
|
|
434
|
+
|
|
435
|
+
rb_define_method(rb_cRactorKeyLockHash, "[]", klh_aref, 1);
|
|
436
|
+
rb_define_method(rb_cRactorKeyLockHash, "fetch", klh_fetch, -1);
|
|
437
|
+
rb_define_method(rb_cRactorKeyLockHash, "key?", klh_key_p, 1);
|
|
438
|
+
rb_define_method(rb_cRactorKeyLockHash, "update", klh_update, 1);
|
|
439
|
+
rb_define_method(rb_cRactorKeyLockHash, "increment", klh_increment, -1);
|
|
440
|
+
rb_define_method(rb_cRactorKeyLockHash, "[]=", klh_aset, 2);
|
|
441
|
+
rb_define_method(rb_cRactorKeyLockHash, "delete", klh_delete, 1);
|
|
442
|
+
rb_define_method(rb_cRactorKeyLockHash, "keys", klh_keys, 0);
|
|
443
|
+
rb_define_method(rb_cRactorKeyLockHash, "to_h", klh_to_h, 0);
|
|
444
|
+
rb_define_method(rb_cRactorKeyLockHash, "inspect", klh_inspect, 0);
|
|
445
|
+
}
|
data/ext/ractor/lock/lock.c
CHANGED
|
@@ -263,6 +263,40 @@ rs_guarded(VALUE self, struct rs_lock *lk, VALUE (*body)(VALUE), void *data,
|
|
|
263
263
|
return rb_ensure(rs_guard_body, (VALUE)&arg, rs_guard_ensure, (VALUE)&arg);
|
|
264
264
|
}
|
|
265
265
|
|
|
266
|
+
/* Hash dups and freezes a bare String key rather than rejecting it, and so do
|
|
267
|
+
* we: the copy is shareable, and the caller's string stays their own. */
|
|
268
|
+
VALUE
|
|
269
|
+
rs_hash_key(VALUE key)
|
|
270
|
+
{
|
|
271
|
+
if (RB_TYPE_P(key, T_STRING) && !RB_OBJ_FROZEN(key)) {
|
|
272
|
+
return rb_str_new_frozen(key);
|
|
273
|
+
}
|
|
274
|
+
return key;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
VALUE
|
|
278
|
+
rs_fixnum_add(VALUE a, VALUE b)
|
|
279
|
+
{
|
|
280
|
+
long x, y;
|
|
281
|
+
|
|
282
|
+
if (!FIXNUM_P(a) || !FIXNUM_P(b)) return Qundef;
|
|
283
|
+
x = FIX2LONG(a); y = FIX2LONG(b);
|
|
284
|
+
if (y > 0 ? x > FIXNUM_MAX - y : x < FIXNUM_MIN - y) return Qundef;
|
|
285
|
+
return LONG2FIX(x + y);
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/* Values are made shareable on the way in: a frozen literal passes through
|
|
289
|
+
* untouched, anything else is deep-frozen IN PLACE. Storing a value here
|
|
290
|
+
* means sharing it. */
|
|
291
|
+
VALUE
|
|
292
|
+
rs_shareable_value(VALUE val)
|
|
293
|
+
{
|
|
294
|
+
if (RB_UNLIKELY(!rb_ractor_shareable_p(val))) {
|
|
295
|
+
return rb_ractor_make_shareable(val);
|
|
296
|
+
}
|
|
297
|
+
return val;
|
|
298
|
+
}
|
|
299
|
+
|
|
266
300
|
void
|
|
267
301
|
rs_lock_init_class(void)
|
|
268
302
|
{
|
|
@@ -285,4 +319,5 @@ Init_lock(void)
|
|
|
285
319
|
rs_lock_init_class();
|
|
286
320
|
Init_lockvar_class();
|
|
287
321
|
Init_lockhash_class();
|
|
322
|
+
Init_keylockhash_class();
|
|
288
323
|
}
|
data/ext/ractor/lock/lock.h
CHANGED
|
@@ -30,6 +30,15 @@ void rs_lock_destroy(struct rs_lock *lk);
|
|
|
30
30
|
void rs_lock_acquire(struct rs_lock *lk);
|
|
31
31
|
void rs_lock_release(struct rs_lock *lk);
|
|
32
32
|
|
|
33
|
+
/* a + b when both are Fixnums and the sum still is; Qundef otherwise. */
|
|
34
|
+
VALUE rs_fixnum_add(VALUE a, VALUE b);
|
|
35
|
+
|
|
36
|
+
/* A stored value is deep-frozen in place unless already shareable. */
|
|
37
|
+
VALUE rs_shareable_value(VALUE val);
|
|
38
|
+
|
|
39
|
+
/* A bare String key becomes a frozen copy, the way Hash stores one. */
|
|
40
|
+
VALUE rs_hash_key(VALUE key);
|
|
41
|
+
|
|
33
42
|
/* Which lock object this thread is inside, or Qnil. */
|
|
34
43
|
VALUE rs_held(VALUE thread);
|
|
35
44
|
void rs_held_set(VALUE thread, VALUE obj);
|
|
@@ -55,5 +64,6 @@ extern VALUE rb_eRactorNestedLock;
|
|
|
55
64
|
void rs_lock_init_class(void);
|
|
56
65
|
void Init_lockvar_class(void);
|
|
57
66
|
void Init_lockhash_class(void);
|
|
67
|
+
void Init_keylockhash_class(void);
|
|
58
68
|
|
|
59
69
|
#endif
|
data/ext/ractor/lock/lockhash.c
CHANGED
|
@@ -132,8 +132,9 @@ lockhash_alloc(VALUE klass)
|
|
|
132
132
|
static int
|
|
133
133
|
lockhash_copy_pair(VALUE key, VALUE value, VALUE dest)
|
|
134
134
|
{
|
|
135
|
+
key = rs_hash_key(key);
|
|
135
136
|
lockhash_check_shareable(key);
|
|
136
|
-
|
|
137
|
+
value = rs_shareable_value(value);
|
|
137
138
|
st_insert(((struct lockhash *)dest)->tbl, (st_data_t)key, (st_data_t)value);
|
|
138
139
|
return ST_CONTINUE;
|
|
139
140
|
}
|
|
@@ -319,9 +320,10 @@ lockhash_to_h(VALUE self)
|
|
|
319
320
|
static VALUE
|
|
320
321
|
lockhash_aset(VALUE self, VALUE key, VALUE value)
|
|
321
322
|
{
|
|
323
|
+
key = rs_hash_key(key);
|
|
322
324
|
lockhash_check_writable(self, "[]=");
|
|
323
325
|
lockhash_check_shareable(key);
|
|
324
|
-
|
|
326
|
+
value = rs_shareable_value(value);
|
|
325
327
|
st_insert(lockhash_ptr(self)->tbl, (st_data_t)key, (st_data_t)value);
|
|
326
328
|
RB_OBJ_WRITTEN(self, Qundef, key);
|
|
327
329
|
RB_OBJ_WRITTEN(self, Qundef, value);
|
data/ext/ractor/lock/lockvar.c
CHANGED
|
@@ -60,14 +60,6 @@ lockvar_ptr(VALUE self)
|
|
|
60
60
|
return lv;
|
|
61
61
|
}
|
|
62
62
|
|
|
63
|
-
static void
|
|
64
|
-
lockvar_check_shareable(VALUE val)
|
|
65
|
-
{
|
|
66
|
-
if (RB_UNLIKELY(!rb_ractor_shareable_p(val))) {
|
|
67
|
-
rb_raise(rb_eArgError, "only shareable object are allowed");
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
|
|
71
63
|
static VALUE
|
|
72
64
|
lockvar_alloc(VALUE klass)
|
|
73
65
|
{
|
|
@@ -84,7 +76,7 @@ lockvar_initialize(int argc, VALUE *argv, VALUE self)
|
|
|
84
76
|
VALUE init = Qnil;
|
|
85
77
|
rb_check_frozen(self); /* send(:initialize) again would write past the lock */
|
|
86
78
|
rb_scan_args(argc, argv, "01", &init);
|
|
87
|
-
|
|
79
|
+
init = rs_shareable_value(init);
|
|
88
80
|
lockvar_ptr(self)->value = init;
|
|
89
81
|
/* Only the guarded slot changes, so the LockVar itself can be frozen and
|
|
90
82
|
* shared; RUBY_TYPED_FROZEN_SHAREABLE is what permits it for a T_DATA. */
|
|
@@ -108,9 +100,8 @@ lockvar_update_body(VALUE ptr)
|
|
|
108
100
|
{
|
|
109
101
|
struct rs_guard_arg *arg = (struct rs_guard_arg *)ptr;
|
|
110
102
|
struct lockvar *lv = lockvar_ptr(arg->self);
|
|
111
|
-
VALUE next = rb_yield(lv->value); /*
|
|
103
|
+
VALUE next = rs_shareable_value(rb_yield(lv->value)); /* guard marked it held */
|
|
112
104
|
|
|
113
|
-
lockvar_check_shareable(next);
|
|
114
105
|
lv->value = next;
|
|
115
106
|
return next;
|
|
116
107
|
}
|
|
@@ -121,9 +112,8 @@ lockvar_increment_body(VALUE ptr)
|
|
|
121
112
|
{
|
|
122
113
|
struct rs_guard_arg *arg = (struct rs_guard_arg *)ptr;
|
|
123
114
|
struct lockvar *lv = lockvar_ptr(arg->self);
|
|
124
|
-
VALUE next = rb_funcall(lv->value, id_plus, 1, (VALUE)arg->data);
|
|
115
|
+
VALUE next = rs_shareable_value(rb_funcall(lv->value, id_plus, 1, (VALUE)arg->data));
|
|
125
116
|
|
|
126
|
-
lockvar_check_shareable(next);
|
|
127
117
|
lv->value = next;
|
|
128
118
|
return next;
|
|
129
119
|
}
|
|
@@ -162,17 +152,6 @@ lockvar_update(VALUE self)
|
|
|
162
152
|
* be interrupted and nothing can re-enter. That lets the whole ceremony go --
|
|
163
153
|
* the held marker, the ensure, the shareable check, the method dispatch -- and
|
|
164
154
|
* leaves the lock and one add. Returns Qundef when it does not apply. */
|
|
165
|
-
static VALUE
|
|
166
|
-
lockvar_fixnum_add(VALUE a, VALUE b)
|
|
167
|
-
{
|
|
168
|
-
long x, y;
|
|
169
|
-
|
|
170
|
-
if (!FIXNUM_P(a) || !FIXNUM_P(b)) return Qundef;
|
|
171
|
-
x = FIX2LONG(a);
|
|
172
|
-
y = FIX2LONG(b);
|
|
173
|
-
if (y > 0 ? x > FIXNUM_MAX - y : x < FIXNUM_MIN - y) return Qundef;
|
|
174
|
-
return LONG2FIX(x + y);
|
|
175
|
-
}
|
|
176
155
|
|
|
177
156
|
static VALUE
|
|
178
157
|
lockvar_increment(int argc, VALUE *argv, VALUE self)
|
|
@@ -189,7 +168,7 @@ lockvar_increment(int argc, VALUE *argv, VALUE self)
|
|
|
189
168
|
VALUE next;
|
|
190
169
|
|
|
191
170
|
rs_lock_acquire(&lv->lock);
|
|
192
|
-
next =
|
|
171
|
+
next = rs_fixnum_add(lv->value, amount);
|
|
193
172
|
if (!RB_UNDEF_P(next)) lv->value = next;
|
|
194
173
|
rs_lock_release(&lv->lock);
|
|
195
174
|
|
data/ext/ractor/tvar/tvar.c
CHANGED
|
@@ -612,13 +612,23 @@ tvar_ptr(VALUE self)
|
|
|
612
612
|
return slot;
|
|
613
613
|
}
|
|
614
614
|
|
|
615
|
+
/* Values are made shareable on the way in: a frozen literal passes through
|
|
616
|
+
* untouched (one shareable_p check), anything else is deep-frozen IN PLACE.
|
|
617
|
+
* Storing a value here means sharing it; freezing it is not a side effect of
|
|
618
|
+
* that, it is the meaning of it. */
|
|
615
619
|
static VALUE
|
|
616
|
-
|
|
620
|
+
tvar_shareable_value(VALUE val)
|
|
617
621
|
{
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
rb_raise(rb_eArgError, "only shareable object are allowed");
|
|
622
|
+
if (RB_UNLIKELY(!rb_ractor_shareable_p(val))) {
|
|
623
|
+
return rb_ractor_make_shareable(val);
|
|
621
624
|
}
|
|
625
|
+
return val;
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
static VALUE
|
|
629
|
+
tvar_new_(VALUE self, VALUE init)
|
|
630
|
+
{
|
|
631
|
+
init = tvar_shareable_value(init);
|
|
622
632
|
|
|
623
633
|
struct tx_global *txg = tx_global_ptr();
|
|
624
634
|
struct tvar_slot *slot;
|
|
@@ -664,9 +674,7 @@ tvar_value(VALUE self)
|
|
|
664
674
|
static VALUE
|
|
665
675
|
tvar_value_set(VALUE self, VALUE val)
|
|
666
676
|
{
|
|
667
|
-
|
|
668
|
-
rb_raise(rb_eArgError, "only shareable object are allowed");
|
|
669
|
-
}
|
|
677
|
+
val = tvar_shareable_value(val);
|
|
670
678
|
|
|
671
679
|
struct tx_logs *tx = tx_logs();
|
|
672
680
|
tx_check(tx);
|
|
@@ -725,12 +733,8 @@ tvar_value_increment_(VALUE self, VALUE inc)
|
|
|
725
733
|
else {
|
|
726
734
|
recv = tx_get(tx, slot, self);
|
|
727
735
|
if (RB_UNLIKELY((ret = tvar_calc_inc(recv, inc)) == Qundef)) {
|
|
728
|
-
/* + can return anything; a TVar only ever holds shareable values
|
|
729
|
-
|
|
730
|
-
ret = rb_funcall(recv, rb_intern("+"), 1, inc);
|
|
731
|
-
if (RB_UNLIKELY(!rb_ractor_shareable_p(ret))) {
|
|
732
|
-
rb_raise(rb_eArgError, "only shareable object are allowed");
|
|
733
|
-
}
|
|
736
|
+
/* + can return anything; a TVar only ever holds shareable values. */
|
|
737
|
+
ret = tvar_shareable_value(rb_funcall(recv, rb_intern("+"), 1, inc));
|
|
734
738
|
}
|
|
735
739
|
tx_set(tx, ret, slot, self);
|
|
736
740
|
}
|
data/lib/ractor/sharing.rb
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
require_relative "sharing/version"
|
|
4
4
|
require_relative "lockvar"
|
|
5
5
|
require_relative "lockhash"
|
|
6
|
+
require_relative "keylockhash"
|
|
6
7
|
require_relative "tvar"
|
|
7
8
|
require_relative "active_object"
|
|
8
9
|
require_relative "actor_hash"
|
|
@@ -15,6 +16,7 @@ class Ractor
|
|
|
15
16
|
# Ractor::TVar one or more variables, changed together -- start here
|
|
16
17
|
# Ractor::LockVar one variable, and a block that runs exactly once
|
|
17
18
|
# Ractor::LockHash a hash of those, atomic across its own keys
|
|
19
|
+
# Ractor::KeyLockHash a hash with one lock per key: parallel across keys
|
|
18
20
|
# Ractor::ActiveObject an object of your own, owned by one Ractor
|
|
19
21
|
# Ractor::ActorHash the same, with the interface already chosen: a hash
|
|
20
22
|
#
|
|
@@ -23,6 +25,7 @@ class Ractor
|
|
|
23
25
|
# require "ractor/tvar"
|
|
24
26
|
# require "ractor/lockvar"
|
|
25
27
|
# require "ractor/lockhash"
|
|
28
|
+
# require "ractor/keylockhash"
|
|
26
29
|
# require "ractor/active_object"
|
|
27
30
|
# require "ractor/actor_hash"
|
|
28
31
|
module Sharing
|