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.
- checksums.yaml +7 -0
- data/LICENSE.txt +21 -0
- data/README.md +261 -0
- data/docs/active_object.md +211 -0
- data/docs/actor_hash.md +144 -0
- data/docs/lockhash.md +150 -0
- data/docs/lockvar.md +280 -0
- data/docs/tvar.md +124 -0
- data/ext/ractor/lock/extconf.rb +2 -0
- data/ext/ractor/lock/lock.c +288 -0
- data/ext/ractor/lock/lock.h +59 -0
- data/ext/ractor/lock/lockhash.c +407 -0
- data/ext/ractor/lock/lockvar.c +259 -0
- data/ext/ractor/tvar/extconf.rb +2 -0
- data/ext/ractor/tvar/tvar.c +869 -0
- data/lib/ractor/active_object/future.rb +58 -0
- data/lib/ractor/active_object.rb +311 -0
- data/lib/ractor/actor_hash.rb +98 -0
- data/lib/ractor/lockhash.rb +3 -0
- data/lib/ractor/lockvar.rb +3 -0
- data/lib/ractor/sharing/version.rb +7 -0
- data/lib/ractor/sharing.rb +30 -0
- data/lib/ractor/tvar.rb +13 -0
- metadata +66 -0
|
@@ -0,0 +1,407 @@
|
|
|
1
|
+
#include "lock.h"
|
|
2
|
+
#include "ruby/st.h"
|
|
3
|
+
|
|
4
|
+
/* Ractor::LockHash - a Hash several Ractors can share.
|
|
5
|
+
*
|
|
6
|
+
* The entries live in an st_table, not in a Ruby Hash. A Hash would be an
|
|
7
|
+
* unshareable object belonging to whichever Ractor built it, and every other
|
|
8
|
+
* Ractor writing into it would be touching another Ractor's object -- a
|
|
9
|
+
* containment violation, and an edge from a shareable object to an unshareable
|
|
10
|
+
* one that only the VM can record. Keys and values are shareable, so with a C
|
|
11
|
+
* table there is no such object at all: the container belongs to nobody.
|
|
12
|
+
*
|
|
13
|
+
* Nothing escapes either: #synchronize yields the LockHash, never the entries.
|
|
14
|
+
* Every write goes through it, and it is a critical section over this one hash:
|
|
15
|
+
* whatever it changes, other Ractors see all of it or none of it.
|
|
16
|
+
*
|
|
17
|
+
* That atomicity costs parallelism. One lock covers the whole hash, so writes
|
|
18
|
+
* to unrelated keys wait for each other; a hash that has to be written to
|
|
19
|
+
* constantly wants one Ractor::LockVar per key instead, and atomicity across
|
|
20
|
+
* separate objects wants Ractor::TVar.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
static VALUE rb_cRactorLockHash;
|
|
24
|
+
|
|
25
|
+
struct lockhash {
|
|
26
|
+
st_table *tbl; /* shareable VALUE => shareable VALUE */
|
|
27
|
+
struct rs_lock lock;
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
/* Keys are shareable, so they are frozen and their hash is stable. */
|
|
31
|
+
static st_index_t
|
|
32
|
+
lockhash_key_hash(st_data_t key)
|
|
33
|
+
{
|
|
34
|
+
return (st_index_t)NUM2LONG(rb_hash((VALUE)key));
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
static int
|
|
38
|
+
lockhash_key_cmp(st_data_t a, st_data_t b)
|
|
39
|
+
{
|
|
40
|
+
return rb_eql((VALUE)a, (VALUE)b) ? 0 : 1;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
static const struct st_hash_type lockhash_type = {
|
|
44
|
+
lockhash_key_cmp,
|
|
45
|
+
lockhash_key_hash,
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
static int
|
|
49
|
+
lockhash_mark_i(st_data_t key, st_data_t value, st_data_t arg)
|
|
50
|
+
{
|
|
51
|
+
rb_gc_mark((VALUE)key);
|
|
52
|
+
rb_gc_mark((VALUE)value);
|
|
53
|
+
return ST_CONTINUE;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/* Everything in the table is shareable already, so Ractor.make_shareable
|
|
57
|
+
* walking in here through the mark function has nothing left to freeze. */
|
|
58
|
+
static void
|
|
59
|
+
lockhash_mark(void *ptr)
|
|
60
|
+
{
|
|
61
|
+
st_foreach(((struct lockhash *)ptr)->tbl, lockhash_mark_i, 0);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
static void
|
|
65
|
+
lockhash_free(void *ptr)
|
|
66
|
+
{
|
|
67
|
+
struct lockhash *lh = ptr;
|
|
68
|
+
st_free_table(lh->tbl);
|
|
69
|
+
rs_lock_destroy(&lh->lock);
|
|
70
|
+
ruby_xfree(lh);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
static size_t
|
|
74
|
+
lockhash_memsize(const void *ptr)
|
|
75
|
+
{
|
|
76
|
+
const struct lockhash *lh = ptr;
|
|
77
|
+
return sizeof(struct lockhash) + st_memsize(lh->tbl);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
static const rb_data_type_t lockhash_data_type = {
|
|
81
|
+
"Ractor::LockHash",
|
|
82
|
+
{lockhash_mark, lockhash_free, lockhash_memsize, NULL},
|
|
83
|
+
0, 0, RUBY_TYPED_FREE_IMMEDIATELY | RUBY_TYPED_FROZEN_SHAREABLE
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
static struct lockhash *
|
|
87
|
+
lockhash_ptr(VALUE self)
|
|
88
|
+
{
|
|
89
|
+
struct lockhash *lh;
|
|
90
|
+
TypedData_Get_Struct(self, struct lockhash, &lockhash_data_type, lh);
|
|
91
|
+
return lh;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
static void
|
|
95
|
+
lockhash_check_shareable(VALUE val)
|
|
96
|
+
{
|
|
97
|
+
if (RB_UNLIKELY(!rb_ractor_shareable_p(val))) {
|
|
98
|
+
rb_raise(rb_eArgError, "only shareable object are allowed");
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/* Writes are only allowed from inside this hash's own #synchronize. Outside it
|
|
103
|
+
* the method is not callable at all, the way a private method is not. */
|
|
104
|
+
static void
|
|
105
|
+
lockhash_check_writable(VALUE self, const char *mid)
|
|
106
|
+
{
|
|
107
|
+
VALUE held = rs_held(rb_thread_current());
|
|
108
|
+
|
|
109
|
+
if (held == self) return;
|
|
110
|
+
|
|
111
|
+
if (NIL_P(held)) {
|
|
112
|
+
rb_raise(rb_eNoMethodError,
|
|
113
|
+
"'%s' is only allowed inside %"PRIsVALUE"#synchronize",
|
|
114
|
+
mid, rb_obj_class(self));
|
|
115
|
+
}
|
|
116
|
+
rb_raise(rb_eRactorNestedLock,
|
|
117
|
+
"already inside another %"PRIsVALUE"; "
|
|
118
|
+
"use Ractor::TVar to change several of them together",
|
|
119
|
+
rb_obj_class(held));
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
static VALUE
|
|
123
|
+
lockhash_alloc(VALUE klass)
|
|
124
|
+
{
|
|
125
|
+
struct lockhash *lh;
|
|
126
|
+
VALUE obj = TypedData_Make_Struct(klass, struct lockhash, &lockhash_data_type, lh);
|
|
127
|
+
lh->tbl = st_init_table(&lockhash_type);
|
|
128
|
+
rs_lock_init(&lh->lock);
|
|
129
|
+
return obj;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
static int
|
|
133
|
+
lockhash_copy_pair(VALUE key, VALUE value, VALUE dest)
|
|
134
|
+
{
|
|
135
|
+
lockhash_check_shareable(key);
|
|
136
|
+
lockhash_check_shareable(value);
|
|
137
|
+
st_insert(((struct lockhash *)dest)->tbl, (st_data_t)key, (st_data_t)value);
|
|
138
|
+
return ST_CONTINUE;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
static VALUE
|
|
142
|
+
lockhash_initialize(int argc, VALUE *argv, VALUE self)
|
|
143
|
+
{
|
|
144
|
+
rb_check_frozen(self); /* send(:initialize) again would write past the lock */
|
|
145
|
+
VALUE init = Qnil;
|
|
146
|
+
|
|
147
|
+
rb_scan_args(argc, argv, "01", &init);
|
|
148
|
+
if (!NIL_P(init)) {
|
|
149
|
+
init = rb_convert_type(init, T_HASH, "Hash", "to_hash");
|
|
150
|
+
rb_hash_foreach(init, lockhash_copy_pair, (VALUE)lockhash_ptr(self));
|
|
151
|
+
}
|
|
152
|
+
/* The LockHash never changes; the hash it guards does. */
|
|
153
|
+
rb_obj_freeze(self);
|
|
154
|
+
rb_ractor_make_shareable(self);
|
|
155
|
+
return self;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/* --- guarded bodies ------------------------------------------------------- */
|
|
159
|
+
|
|
160
|
+
struct lockhash_op {
|
|
161
|
+
VALUE key;
|
|
162
|
+
VALUE value;
|
|
163
|
+
int argc;
|
|
164
|
+
};
|
|
165
|
+
|
|
166
|
+
/* Qundef when absent. */
|
|
167
|
+
static VALUE
|
|
168
|
+
lockhash_lookup(VALUE self, VALUE key)
|
|
169
|
+
{
|
|
170
|
+
st_data_t found;
|
|
171
|
+
return st_lookup(lockhash_ptr(self)->tbl, (st_data_t)key, &found) ? (VALUE)found : Qundef;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
static VALUE
|
|
175
|
+
lockhash_aref_body(VALUE ptr)
|
|
176
|
+
{
|
|
177
|
+
struct rs_guard_arg *arg = (struct rs_guard_arg *)ptr;
|
|
178
|
+
struct lockhash_op *op = arg->data;
|
|
179
|
+
VALUE found = lockhash_lookup(arg->self, op->key);
|
|
180
|
+
return RB_UNDEF_P(found) ? Qnil : found;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
static VALUE
|
|
184
|
+
lockhash_fetch_body(VALUE ptr)
|
|
185
|
+
{
|
|
186
|
+
struct rs_guard_arg *arg = (struct rs_guard_arg *)ptr;
|
|
187
|
+
struct lockhash_op *op = arg->data;
|
|
188
|
+
return lockhash_lookup(arg->self, op->key);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
static VALUE
|
|
192
|
+
lockhash_key_p_body(VALUE ptr)
|
|
193
|
+
{
|
|
194
|
+
struct rs_guard_arg *arg = (struct rs_guard_arg *)ptr;
|
|
195
|
+
struct lockhash_op *op = arg->data;
|
|
196
|
+
return RB_UNDEF_P(lockhash_lookup(arg->self, op->key)) ? Qfalse : Qtrue;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
static int
|
|
200
|
+
lockhash_collect_key(st_data_t key, st_data_t value, st_data_t ary)
|
|
201
|
+
{
|
|
202
|
+
rb_ary_push((VALUE)ary, (VALUE)key);
|
|
203
|
+
return ST_CONTINUE;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
static int
|
|
207
|
+
lockhash_collect_pair(st_data_t key, st_data_t value, st_data_t hash)
|
|
208
|
+
{
|
|
209
|
+
rb_hash_aset((VALUE)hash, (VALUE)key, (VALUE)value);
|
|
210
|
+
return ST_CONTINUE;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/* A Hash of the entries as they are now; the caller freezes it if it escapes. */
|
|
214
|
+
static VALUE
|
|
215
|
+
lockhash_snapshot(VALUE self)
|
|
216
|
+
{
|
|
217
|
+
VALUE hash = rb_hash_new();
|
|
218
|
+
st_foreach(lockhash_ptr(self)->tbl, lockhash_collect_pair, (st_data_t)hash);
|
|
219
|
+
return hash;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
static VALUE
|
|
223
|
+
lockhash_keys_body(VALUE ptr)
|
|
224
|
+
{
|
|
225
|
+
struct rs_guard_arg *arg = (struct rs_guard_arg *)ptr;
|
|
226
|
+
VALUE keys = rb_ary_new_capa(lockhash_ptr(arg->self)->tbl->num_entries);
|
|
227
|
+
st_foreach(lockhash_ptr(arg->self)->tbl, lockhash_collect_key, (st_data_t)keys);
|
|
228
|
+
return rb_ractor_make_shareable(keys);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
static VALUE
|
|
232
|
+
lockhash_to_h_body(VALUE ptr)
|
|
233
|
+
{
|
|
234
|
+
struct rs_guard_arg *arg = (struct rs_guard_arg *)ptr;
|
|
235
|
+
return rb_ractor_make_shareable(lockhash_snapshot(arg->self));
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
static VALUE
|
|
239
|
+
lockhash_sync_body(VALUE ptr)
|
|
240
|
+
{
|
|
241
|
+
struct rs_guard_arg *arg = (struct rs_guard_arg *)ptr;
|
|
242
|
+
return rb_yield(arg->self); /* rs_guarded has marked the lock held */
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/* --- reads ---------------------------------------------------------------- */
|
|
246
|
+
|
|
247
|
+
#define LOCKHASH_READ(self, body, op) \
|
|
248
|
+
rs_guarded((self), &lockhash_ptr(self)->lock, (body), (op), true, "")
|
|
249
|
+
|
|
250
|
+
/*
|
|
251
|
+
* call-seq:
|
|
252
|
+
* lockhash[key] -> value
|
|
253
|
+
*
|
|
254
|
+
* Reads one entry under the lock, so it never sees a #synchronize half done.
|
|
255
|
+
*/
|
|
256
|
+
static VALUE
|
|
257
|
+
lockhash_aref(VALUE self, VALUE key)
|
|
258
|
+
{
|
|
259
|
+
struct lockhash_op op = {key, Qnil, 1};
|
|
260
|
+
return LOCKHASH_READ(self, lockhash_aref_body, &op);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
static VALUE
|
|
264
|
+
lockhash_fetch(int argc, VALUE *argv, VALUE self)
|
|
265
|
+
{
|
|
266
|
+
struct lockhash_op op = {Qnil, Qnil, argc};
|
|
267
|
+
VALUE found;
|
|
268
|
+
|
|
269
|
+
rb_scan_args(argc, argv, "11", &op.key, &op.value);
|
|
270
|
+
found = LOCKHASH_READ(self, lockhash_fetch_body, &op);
|
|
271
|
+
if (!RB_UNDEF_P(found)) return found;
|
|
272
|
+
|
|
273
|
+
/* The default runs with the lock released. Held, a block that touched this
|
|
274
|
+
* hash again would wait for a lock its own frame is holding. */
|
|
275
|
+
if (rb_block_given_p()) return rb_yield(op.key);
|
|
276
|
+
if (argc > 1) return op.value;
|
|
277
|
+
rb_raise(rb_eKeyError, "key not found: %+"PRIsVALUE, op.key);
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
static VALUE
|
|
281
|
+
lockhash_key_p(VALUE self, VALUE key)
|
|
282
|
+
{
|
|
283
|
+
struct lockhash_op op = {key, Qnil, 1};
|
|
284
|
+
return LOCKHASH_READ(self, lockhash_key_p_body, &op);
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
/*
|
|
288
|
+
* call-seq:
|
|
289
|
+
* lockhash.keys -> array
|
|
290
|
+
* lockhash.to_h -> hash
|
|
291
|
+
*
|
|
292
|
+
* A snapshot taken under the lock, frozen and shareable, of the whole hash as
|
|
293
|
+
* it was at one moment.
|
|
294
|
+
*/
|
|
295
|
+
static VALUE
|
|
296
|
+
lockhash_keys(VALUE self)
|
|
297
|
+
{
|
|
298
|
+
return LOCKHASH_READ(self, lockhash_keys_body, NULL);
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
static VALUE
|
|
302
|
+
lockhash_to_h(VALUE self)
|
|
303
|
+
{
|
|
304
|
+
return LOCKHASH_READ(self, lockhash_to_h_body, NULL);
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
/* --- writes: only from inside #synchronize -------------------------------- */
|
|
308
|
+
|
|
309
|
+
/*
|
|
310
|
+
* call-seq:
|
|
311
|
+
* lockhash[key] = value -> value
|
|
312
|
+
*
|
|
313
|
+
* Stores one entry. Only allowed inside this hash's #synchronize, so that
|
|
314
|
+
* reading an entry and writing it back is one step and not two.
|
|
315
|
+
*/
|
|
316
|
+
static VALUE
|
|
317
|
+
lockhash_aset(VALUE self, VALUE key, VALUE value)
|
|
318
|
+
{
|
|
319
|
+
lockhash_check_writable(self, "[]=");
|
|
320
|
+
lockhash_check_shareable(key);
|
|
321
|
+
lockhash_check_shareable(value);
|
|
322
|
+
st_insert(lockhash_ptr(self)->tbl, (st_data_t)key, (st_data_t)value);
|
|
323
|
+
RB_OBJ_WRITTEN(self, Qundef, key);
|
|
324
|
+
RB_OBJ_WRITTEN(self, Qundef, value);
|
|
325
|
+
return value;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
static VALUE
|
|
329
|
+
lockhash_delete(VALUE self, VALUE key)
|
|
330
|
+
{
|
|
331
|
+
st_data_t k = (st_data_t)key, v;
|
|
332
|
+
|
|
333
|
+
lockhash_check_writable(self, "delete");
|
|
334
|
+
return st_delete(lockhash_ptr(self)->tbl, &k, &v) ? (VALUE)v : Qnil;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
static VALUE
|
|
338
|
+
lockhash_clear(VALUE self)
|
|
339
|
+
{
|
|
340
|
+
lockhash_check_writable(self, "clear");
|
|
341
|
+
st_clear(lockhash_ptr(self)->tbl);
|
|
342
|
+
return self;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
/*
|
|
346
|
+
* call-seq:
|
|
347
|
+
* lockhash.synchronize {|lockhash| ... } -> object
|
|
348
|
+
*
|
|
349
|
+
* Runs the block holding this hash, and returns the block's value. Everything
|
|
350
|
+
* it changes becomes visible together. The block is handed the LockHash
|
|
351
|
+
* itself, never the Hash inside it, so no reference to the state escapes.
|
|
352
|
+
*
|
|
353
|
+
* board.synchronize {|b| b[:me] = score; b.delete(:stale) }
|
|
354
|
+
*
|
|
355
|
+
* Keep it short: every reader and writer of this hash waits for it.
|
|
356
|
+
*/
|
|
357
|
+
static VALUE
|
|
358
|
+
lockhash_synchronize(VALUE self)
|
|
359
|
+
{
|
|
360
|
+
rb_need_block();
|
|
361
|
+
return rs_guarded(self, &lockhash_ptr(self)->lock, lockhash_sync_body, NULL, true, "");
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
static VALUE
|
|
365
|
+
lockhash_inspect_body(VALUE ptr)
|
|
366
|
+
{
|
|
367
|
+
struct rs_guard_arg *arg = (struct rs_guard_arg *)ptr;
|
|
368
|
+
return rb_sprintf("#<%"PRIsVALUE" %+"PRIsVALUE">",
|
|
369
|
+
rb_obj_class(arg->self), lockhash_snapshot(arg->self));
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
/* Reading the table needs the lock, so inspect takes it -- unless this thread
|
|
373
|
+
* is inside some other lock, where taking it could deadlock and inspect must
|
|
374
|
+
* not raise either. Then it just says nothing about the contents. */
|
|
375
|
+
static VALUE
|
|
376
|
+
lockhash_inspect(VALUE self)
|
|
377
|
+
{
|
|
378
|
+
VALUE held = rs_held(rb_thread_current());
|
|
379
|
+
|
|
380
|
+
if (!NIL_P(held) && held != self) {
|
|
381
|
+
return rb_sprintf("#<%"PRIsVALUE" ...>", rb_obj_class(self));
|
|
382
|
+
}
|
|
383
|
+
return LOCKHASH_READ(self, lockhash_inspect_body, NULL);
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
void
|
|
387
|
+
Init_lockhash_class(void)
|
|
388
|
+
{
|
|
389
|
+
rb_cRactorLockHash = rb_define_class_under(rb_cRactor, "LockHash", rb_cObject);
|
|
390
|
+
rb_define_alloc_func(rb_cRactorLockHash, lockhash_alloc);
|
|
391
|
+
rb_define_method(rb_cRactorLockHash, "initialize", lockhash_initialize, -1);
|
|
392
|
+
|
|
393
|
+
rb_define_method(rb_cRactorLockHash, "[]", lockhash_aref, 1);
|
|
394
|
+
rb_define_method(rb_cRactorLockHash, "fetch", lockhash_fetch, -1);
|
|
395
|
+
rb_define_method(rb_cRactorLockHash, "key?", lockhash_key_p, 1);
|
|
396
|
+
rb_define_method(rb_cRactorLockHash, "keys", lockhash_keys, 0);
|
|
397
|
+
rb_define_method(rb_cRactorLockHash, "to_h", lockhash_to_h, 0);
|
|
398
|
+
|
|
399
|
+
rb_define_method(rb_cRactorLockHash, "[]=", lockhash_aset, 2);
|
|
400
|
+
rb_define_method(rb_cRactorLockHash, "delete", lockhash_delete, 1);
|
|
401
|
+
rb_define_method(rb_cRactorLockHash, "clear", lockhash_clear, 0);
|
|
402
|
+
rb_define_method(rb_cRactorLockHash, "synchronize", lockhash_synchronize, 0);
|
|
403
|
+
|
|
404
|
+
rb_define_method(rb_cRactorLockHash, "inspect", lockhash_inspect, 0);
|
|
405
|
+
|
|
406
|
+
rb_define_const(rb_cRactorLockHash, "NestedLockError", rb_eRactorNestedLock);
|
|
407
|
+
}
|
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
#include "lock.h"
|
|
2
|
+
|
|
3
|
+
/* Ractor::LockVar - one shareable value, replaced under a lock.
|
|
4
|
+
*
|
|
5
|
+
* It is a variable, not a lock: there is no lock/unlock and no owner query.
|
|
6
|
+
* The lock is only ever held for the duration of #value, #update or
|
|
7
|
+
* #increment, all written here in C so that no interrupt can be delivered
|
|
8
|
+
* between taking the lock and arming the ensure that releases it.
|
|
9
|
+
*
|
|
10
|
+
* #value takes the lock too, so an update's block is atomic as far as readers
|
|
11
|
+
* are concerned, not just its final store. That is what an update block needs
|
|
12
|
+
* if it makes anything else observable:
|
|
13
|
+
*
|
|
14
|
+
* lv.update {|v| $last = v; v + 1 } # readers always see lv.value > $last
|
|
15
|
+
*
|
|
16
|
+
* Touching any LockVar from inside an update is refused, its own included: the
|
|
17
|
+
* block is already given the value, and a nested update's write would be
|
|
18
|
+
* discarded by the outer block's result.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
static VALUE rb_cRactorLockVar;
|
|
22
|
+
static ID id_plus;
|
|
23
|
+
|
|
24
|
+
struct lockvar {
|
|
25
|
+
VALUE value;
|
|
26
|
+
struct rs_lock lock;
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
static void
|
|
30
|
+
lockvar_mark(void *ptr)
|
|
31
|
+
{
|
|
32
|
+
rb_gc_mark(((struct lockvar *)ptr)->value);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
static void
|
|
36
|
+
lockvar_free(void *ptr)
|
|
37
|
+
{
|
|
38
|
+
struct lockvar *lv = ptr;
|
|
39
|
+
rs_lock_destroy(&lv->lock);
|
|
40
|
+
ruby_xfree(lv);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
static size_t
|
|
44
|
+
lockvar_memsize(const void *ptr)
|
|
45
|
+
{
|
|
46
|
+
return sizeof(struct lockvar);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
static const rb_data_type_t lockvar_data_type = {
|
|
50
|
+
"Ractor::LockVar",
|
|
51
|
+
{lockvar_mark, lockvar_free, lockvar_memsize, NULL},
|
|
52
|
+
0, 0, RUBY_TYPED_FREE_IMMEDIATELY | RUBY_TYPED_FROZEN_SHAREABLE
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
static struct lockvar *
|
|
56
|
+
lockvar_ptr(VALUE self)
|
|
57
|
+
{
|
|
58
|
+
struct lockvar *lv;
|
|
59
|
+
TypedData_Get_Struct(self, struct lockvar, &lockvar_data_type, lv);
|
|
60
|
+
return lv;
|
|
61
|
+
}
|
|
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
|
+
static VALUE
|
|
72
|
+
lockvar_alloc(VALUE klass)
|
|
73
|
+
{
|
|
74
|
+
struct lockvar *lv;
|
|
75
|
+
VALUE obj = TypedData_Make_Struct(klass, struct lockvar, &lockvar_data_type, lv);
|
|
76
|
+
lv->value = Qnil;
|
|
77
|
+
rs_lock_init(&lv->lock);
|
|
78
|
+
return obj;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
static VALUE
|
|
82
|
+
lockvar_initialize(int argc, VALUE *argv, VALUE self)
|
|
83
|
+
{
|
|
84
|
+
VALUE init = Qnil;
|
|
85
|
+
rb_check_frozen(self); /* send(:initialize) again would write past the lock */
|
|
86
|
+
rb_scan_args(argc, argv, "01", &init);
|
|
87
|
+
lockvar_check_shareable(init);
|
|
88
|
+
lockvar_ptr(self)->value = init;
|
|
89
|
+
/* Only the guarded slot changes, so the LockVar itself can be frozen and
|
|
90
|
+
* shared; RUBY_TYPED_FROZEN_SHAREABLE is what permits it for a T_DATA. */
|
|
91
|
+
rb_obj_freeze(self);
|
|
92
|
+
rb_ractor_make_shareable(self);
|
|
93
|
+
return self;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/* --- guarded bodies ------------------------------------------------------- */
|
|
97
|
+
|
|
98
|
+
static VALUE
|
|
99
|
+
lockvar_value_body(VALUE ptr)
|
|
100
|
+
{
|
|
101
|
+
struct rs_guard_arg *arg = (struct rs_guard_arg *)ptr;
|
|
102
|
+
return lockvar_ptr(arg->self)->value;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/* yields the value and stores what the block returns */
|
|
106
|
+
static VALUE
|
|
107
|
+
lockvar_update_body(VALUE ptr)
|
|
108
|
+
{
|
|
109
|
+
struct rs_guard_arg *arg = (struct rs_guard_arg *)ptr;
|
|
110
|
+
struct lockvar *lv = lockvar_ptr(arg->self);
|
|
111
|
+
VALUE next = rb_yield(lv->value); /* rs_guarded has marked the lock held */
|
|
112
|
+
|
|
113
|
+
lockvar_check_shareable(next);
|
|
114
|
+
lv->value = next;
|
|
115
|
+
return next;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/* adds amount to the value; + may be anything, so the held marker is set too */
|
|
119
|
+
static VALUE
|
|
120
|
+
lockvar_increment_body(VALUE ptr)
|
|
121
|
+
{
|
|
122
|
+
struct rs_guard_arg *arg = (struct rs_guard_arg *)ptr;
|
|
123
|
+
struct lockvar *lv = lockvar_ptr(arg->self);
|
|
124
|
+
VALUE next = rb_funcall(lv->value, id_plus, 1, (VALUE)arg->data);
|
|
125
|
+
|
|
126
|
+
lockvar_check_shareable(next);
|
|
127
|
+
lv->value = next;
|
|
128
|
+
return next;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/* --- methods -------------------------------------------------------------- */
|
|
132
|
+
|
|
133
|
+
/*
|
|
134
|
+
* call-seq:
|
|
135
|
+
* lockvar.update {|value| new_value } -> new_value
|
|
136
|
+
*
|
|
137
|
+
* Replaces the value with what the block returns, with nothing else able to
|
|
138
|
+
* read it in between, and returns the new value.
|
|
139
|
+
*
|
|
140
|
+
* Compute the new value from the one the block is given. Reading it outside
|
|
141
|
+
* and using it inside loses whatever landed in between:
|
|
142
|
+
*
|
|
143
|
+
* v = lv.value; lv.update { v + 1 } # wrong: v is stale
|
|
144
|
+
* lv.update { it + 1 } # right
|
|
145
|
+
*/
|
|
146
|
+
static VALUE
|
|
147
|
+
lockvar_update(VALUE self)
|
|
148
|
+
{
|
|
149
|
+
rb_need_block();
|
|
150
|
+
return rs_guarded(self, &lockvar_ptr(self)->lock, lockvar_update_body, NULL, false,
|
|
151
|
+
"the inner result would be discarded by the outer block");
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/*
|
|
155
|
+
* call-seq:
|
|
156
|
+
* lockvar.increment(amount = 1) -> new value
|
|
157
|
+
*
|
|
158
|
+
* Adds +amount+ to the value under the lock and returns the result; the same as
|
|
159
|
+
* <code>update {|v| v + amount }</code>.
|
|
160
|
+
*/
|
|
161
|
+
/* Fixnum + Fixnum, when the sum is still a Fixnum: no Ruby runs, so nothing can
|
|
162
|
+
* be interrupted and nothing can re-enter. That lets the whole ceremony go --
|
|
163
|
+
* the held marker, the ensure, the shareable check, the method dispatch -- and
|
|
164
|
+
* 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
|
+
|
|
177
|
+
static VALUE
|
|
178
|
+
lockvar_increment(int argc, VALUE *argv, VALUE self)
|
|
179
|
+
{
|
|
180
|
+
struct lockvar *lv = lockvar_ptr(self);
|
|
181
|
+
VALUE amount;
|
|
182
|
+
|
|
183
|
+
/* Not rb_scan_args: an explicit nil is an argument, and value + nil raises,
|
|
184
|
+
* where an omitted one means 1. */
|
|
185
|
+
rb_check_arity(argc, 0, 1);
|
|
186
|
+
amount = argc == 0 ? INT2FIX(1) : argv[0];
|
|
187
|
+
|
|
188
|
+
if (FIXNUM_P(amount) && NIL_P(rs_held(rb_thread_current()))) {
|
|
189
|
+
VALUE next;
|
|
190
|
+
|
|
191
|
+
rs_lock_acquire(&lv->lock);
|
|
192
|
+
next = lockvar_fixnum_add(lv->value, amount);
|
|
193
|
+
if (!RB_UNDEF_P(next)) lv->value = next;
|
|
194
|
+
rs_lock_release(&lv->lock);
|
|
195
|
+
|
|
196
|
+
if (!RB_UNDEF_P(next)) return next;
|
|
197
|
+
}
|
|
198
|
+
return rs_guarded(self, &lv->lock, lockvar_increment_body,
|
|
199
|
+
(void *)amount, false, "the block was given that value already");
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/*
|
|
203
|
+
* call-seq:
|
|
204
|
+
* lockvar.value -> object
|
|
205
|
+
*
|
|
206
|
+
* Reads the value under the lock, so it waits for an update in flight and never
|
|
207
|
+
* sees that update's block half done. Raises NestedLockError if this thread is
|
|
208
|
+
* inside any update.
|
|
209
|
+
*
|
|
210
|
+
* This is a snapshot: true when taken, possibly stale by the time it is used.
|
|
211
|
+
* Never feed it back into #update.
|
|
212
|
+
*/
|
|
213
|
+
static VALUE
|
|
214
|
+
lockvar_value(VALUE self)
|
|
215
|
+
{
|
|
216
|
+
struct lockvar *lv = lockvar_ptr(self);
|
|
217
|
+
VALUE v;
|
|
218
|
+
|
|
219
|
+
/* Inside any update, this is the nesting error, and rs_guarded raises it. */
|
|
220
|
+
if (RB_UNLIKELY(!NIL_P(rs_held(rb_thread_current())))) {
|
|
221
|
+
return rs_guarded(self, &lv->lock, lockvar_value_body, NULL, false,
|
|
222
|
+
"the block was given that value already");
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/* Otherwise: take the lock, read one word, put it back. No Ruby runs in
|
|
226
|
+
* between, so there is no interrupt to guard against and no reason to mark
|
|
227
|
+
* the lock held -- which is what the ensure and the marker are for. */
|
|
228
|
+
rs_lock_acquire(&lv->lock);
|
|
229
|
+
v = lv->value;
|
|
230
|
+
rs_lock_release(&lv->lock);
|
|
231
|
+
return v;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/* Reads the value without the lock, on purpose. inspect is for a debugger, a
|
|
235
|
+
* `p`, an exception message: it must not block and must not raise, and a value
|
|
236
|
+
* is one word, so an unlocked read gets some real value, just possibly a stale
|
|
237
|
+
* one. That is the right trade for a display. */
|
|
238
|
+
static VALUE
|
|
239
|
+
lockvar_inspect(VALUE self)
|
|
240
|
+
{
|
|
241
|
+
return rb_sprintf("#<%"PRIsVALUE" %+"PRIsVALUE">",
|
|
242
|
+
rb_obj_class(self), lockvar_ptr(self)->value);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
void
|
|
246
|
+
Init_lockvar_class(void)
|
|
247
|
+
{
|
|
248
|
+
id_plus = rb_intern("+");
|
|
249
|
+
|
|
250
|
+
rb_cRactorLockVar = rb_define_class_under(rb_cRactor, "LockVar", rb_cObject);
|
|
251
|
+
rb_define_alloc_func(rb_cRactorLockVar, lockvar_alloc);
|
|
252
|
+
rb_define_method(rb_cRactorLockVar, "initialize", lockvar_initialize, -1);
|
|
253
|
+
rb_define_method(rb_cRactorLockVar, "value", lockvar_value, 0);
|
|
254
|
+
rb_define_method(rb_cRactorLockVar, "update", lockvar_update, 0);
|
|
255
|
+
rb_define_method(rb_cRactorLockVar, "increment", lockvar_increment, -1);
|
|
256
|
+
rb_define_method(rb_cRactorLockVar, "inspect", lockvar_inspect, 0);
|
|
257
|
+
|
|
258
|
+
rb_define_const(rb_cRactorLockVar, "NestedLockError", rb_eRactorNestedLock);
|
|
259
|
+
}
|