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.
@@ -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
+ }
@@ -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
  }
@@ -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
@@ -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
- lockhash_check_shareable(value);
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
- lockhash_check_shareable(value);
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);
@@ -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
- lockvar_check_shareable(init);
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); /* rs_guarded has marked the lock held */
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 = lockvar_fixnum_add(lv->value, amount);
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
 
@@ -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
- tvar_new_(VALUE self, VALUE init)
620
+ tvar_shareable_value(VALUE val)
617
621
  {
618
- // init should be shareable
619
- if (RB_UNLIKELY(!rb_ractor_shareable_p(init))) {
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
- if (RB_UNLIKELY(!rb_ractor_shareable_p(val))) {
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, and
729
- * outside a transaction this path is checked by #value=. */
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
  }
@@ -0,0 +1,3 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "ractor/lock.so"
@@ -2,6 +2,6 @@
2
2
 
3
3
  class Ractor
4
4
  module Sharing
5
- VERSION = "0.2.0"
5
+ VERSION = "0.3.0"
6
6
  end
7
7
  end
@@ -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