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,288 @@
|
|
|
1
|
+
#include "lock.h"
|
|
2
|
+
|
|
3
|
+
VALUE rb_eRactorNestedLock;
|
|
4
|
+
static VALUE rb_eRactorClosed;
|
|
5
|
+
static ID id_new, id_receive, id_push, id_held;
|
|
6
|
+
static VALUE sym_wakeup;
|
|
7
|
+
|
|
8
|
+
void
|
|
9
|
+
rs_lock_init(struct rs_lock *lk)
|
|
10
|
+
{
|
|
11
|
+
lk->locked = false;
|
|
12
|
+
lk->head = lk->tail = NULL;
|
|
13
|
+
rb_native_mutex_initialize(&lk->mutex);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
void
|
|
17
|
+
rs_lock_destroy(struct rs_lock *lk)
|
|
18
|
+
{
|
|
19
|
+
rb_native_mutex_destroy(&lk->mutex);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/* --- waiter queue: caller holds the native mutex ------------------------- */
|
|
23
|
+
|
|
24
|
+
static void
|
|
25
|
+
rs_enqueue(struct rs_lock *lk, struct rs_waiter *w)
|
|
26
|
+
{
|
|
27
|
+
w->next = NULL;
|
|
28
|
+
if (lk->tail) lk->tail->next = w; else lk->head = w;
|
|
29
|
+
lk->tail = w;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
static void
|
|
33
|
+
rs_unlink(struct rs_lock *lk, struct rs_waiter *target)
|
|
34
|
+
{
|
|
35
|
+
struct rs_waiter **pp = &lk->head, *prev = NULL;
|
|
36
|
+
while (*pp) {
|
|
37
|
+
if (*pp == target) {
|
|
38
|
+
*pp = target->next;
|
|
39
|
+
if (lk->tail == target) lk->tail = prev;
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
prev = *pp;
|
|
43
|
+
pp = &(*pp)->next;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/* Copies the VALUE, not the node: the node lives on a stack frame that may be
|
|
48
|
+
* gone once the mutex is released. */
|
|
49
|
+
static VALUE
|
|
50
|
+
rs_next_port(struct rs_lock *lk)
|
|
51
|
+
{
|
|
52
|
+
return lk->head ? lk->head->port : Qfalse;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
static VALUE
|
|
56
|
+
rs_send_wakeup(VALUE port)
|
|
57
|
+
{
|
|
58
|
+
rb_funcall(port, id_push, 1, sym_wakeup);
|
|
59
|
+
return Qtrue;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/* The waiter's Ractor is gone, which closed its port: that wakeup is moot. */
|
|
63
|
+
static VALUE
|
|
64
|
+
rs_wakeup_lost(VALUE unused, VALUE err)
|
|
65
|
+
{
|
|
66
|
+
return Qfalse;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/* Wakes the first waiter, if the lock is free and anybody is queued. Runs Ruby
|
|
70
|
+
* code, so the native mutex must be released. Waking just one is enough: only
|
|
71
|
+
* one thread can take the lock, and a waiter that leaves without taking it
|
|
72
|
+
* wakes the next. A waiter whose port has been closed is skipped. */
|
|
73
|
+
static void
|
|
74
|
+
rs_wake_next(struct rs_lock *lk)
|
|
75
|
+
{
|
|
76
|
+
VALUE failed = Qfalse;
|
|
77
|
+
int i;
|
|
78
|
+
|
|
79
|
+
for (i = 0; i < 8; i++) {
|
|
80
|
+
VALUE port;
|
|
81
|
+
|
|
82
|
+
rb_native_mutex_lock(&lk->mutex);
|
|
83
|
+
port = lk->locked ? Qfalse : rs_next_port(lk);
|
|
84
|
+
rb_native_mutex_unlock(&lk->mutex);
|
|
85
|
+
|
|
86
|
+
if (!port || port == failed) return;
|
|
87
|
+
if (RTEST(rb_rescue2(rs_send_wakeup, port, rs_wakeup_lost, Qnil,
|
|
88
|
+
rb_eRactorClosed, (VALUE)0))) {
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
failed = port;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/* --- acquire / release --------------------------------------------------- */
|
|
96
|
+
|
|
97
|
+
struct rs_wait_arg {
|
|
98
|
+
struct rs_lock *lk;
|
|
99
|
+
struct rs_waiter waiter;
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
static VALUE
|
|
103
|
+
rs_wait_body(VALUE ptr)
|
|
104
|
+
{
|
|
105
|
+
struct rs_wait_arg *arg = (struct rs_wait_arg *)ptr;
|
|
106
|
+
return rb_funcall(arg->waiter.port, id_receive, 0);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/* Leaving the queue, woken or interrupted. If the lock is free and somebody is
|
|
110
|
+
* still queued, pass the wakeup on: the signal that reached us may have been
|
|
111
|
+
* meant for the queue, and nobody else is going to send another one. */
|
|
112
|
+
static VALUE
|
|
113
|
+
rs_wait_ensure(VALUE ptr)
|
|
114
|
+
{
|
|
115
|
+
struct rs_wait_arg *arg = (struct rs_wait_arg *)ptr;
|
|
116
|
+
|
|
117
|
+
rb_native_mutex_lock(&arg->lk->mutex);
|
|
118
|
+
rs_unlink(arg->lk, &arg->waiter);
|
|
119
|
+
rb_native_mutex_unlock(&arg->lk->mutex);
|
|
120
|
+
|
|
121
|
+
rs_wake_next(arg->lk);
|
|
122
|
+
return Qnil;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
void
|
|
126
|
+
rs_lock_acquire(struct rs_lock *lk)
|
|
127
|
+
{
|
|
128
|
+
struct rs_wait_arg arg;
|
|
129
|
+
bool have_port = false;
|
|
130
|
+
|
|
131
|
+
for (;;) {
|
|
132
|
+
rb_native_mutex_lock(&lk->mutex);
|
|
133
|
+
if (!lk->locked) {
|
|
134
|
+
lk->locked = true;
|
|
135
|
+
rb_native_mutex_unlock(&lk->mutex);
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
rb_native_mutex_unlock(&lk->mutex);
|
|
139
|
+
|
|
140
|
+
if (!have_port) {
|
|
141
|
+
/* Allocating can trigger a GC, so never under the mutex. */
|
|
142
|
+
arg.lk = lk;
|
|
143
|
+
arg.waiter.next = NULL;
|
|
144
|
+
arg.waiter.port = rb_funcall(rb_const_get(rb_cRactor, rb_intern("Port")), id_new, 0);
|
|
145
|
+
have_port = true;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
rb_native_mutex_lock(&lk->mutex);
|
|
149
|
+
if (!lk->locked) {
|
|
150
|
+
lk->locked = true;
|
|
151
|
+
rb_native_mutex_unlock(&lk->mutex);
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
rs_enqueue(lk, &arg.waiter);
|
|
155
|
+
rb_native_mutex_unlock(&lk->mutex);
|
|
156
|
+
|
|
157
|
+
/* Parks through the VM scheduler: interruptible and M:N friendly.
|
|
158
|
+
* A spurious wakeup only costs another turn around this loop. */
|
|
159
|
+
rb_ensure(rs_wait_body, (VALUE)&arg, rs_wait_ensure, (VALUE)&arg);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
void
|
|
164
|
+
rs_lock_release(struct rs_lock *lk)
|
|
165
|
+
{
|
|
166
|
+
bool waiting;
|
|
167
|
+
|
|
168
|
+
rb_native_mutex_lock(&lk->mutex);
|
|
169
|
+
lk->locked = false;
|
|
170
|
+
waiting = lk->head != NULL;
|
|
171
|
+
rb_native_mutex_unlock(&lk->mutex);
|
|
172
|
+
|
|
173
|
+
/* Nobody queued is the common case, and it is already answered above: taking
|
|
174
|
+
* the mutex a second time to find that out would double the cost of an
|
|
175
|
+
* uncontended release. */
|
|
176
|
+
if (!waiting) return;
|
|
177
|
+
|
|
178
|
+
/* Waiters stay queued until they wake by themselves, so a wakeup lost to an
|
|
179
|
+
* interrupt here is retried by the next release. */
|
|
180
|
+
rs_wake_next(lk);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/* --- held marker / guarded ------------------------------------------------ */
|
|
184
|
+
|
|
185
|
+
VALUE
|
|
186
|
+
rs_held(VALUE thread)
|
|
187
|
+
{
|
|
188
|
+
return rb_attr_get(thread, id_held);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
void
|
|
192
|
+
rs_held_set(VALUE thread, VALUE obj)
|
|
193
|
+
{
|
|
194
|
+
rb_ivar_set(thread, id_held, obj);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
static VALUE
|
|
198
|
+
rs_restore_held(VALUE ptr)
|
|
199
|
+
{
|
|
200
|
+
struct rs_guard_arg *arg = (struct rs_guard_arg *)ptr;
|
|
201
|
+
rs_held_set(arg->thread, arg->prev_held);
|
|
202
|
+
return Qnil;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/* The marker lives in an ivar on the Thread, so restoring it runs Ruby-visible
|
|
206
|
+
* code and can raise -- a frozen Thread does. Raising here used to skip the
|
|
207
|
+
* release below and strand the lock for the life of the process. */
|
|
208
|
+
static VALUE
|
|
209
|
+
rs_guard_ensure(VALUE ptr)
|
|
210
|
+
{
|
|
211
|
+
struct rs_guard_arg *arg = (struct rs_guard_arg *)ptr;
|
|
212
|
+
int state = 0;
|
|
213
|
+
|
|
214
|
+
rb_protect(rs_restore_held, (VALUE)arg, &state);
|
|
215
|
+
rs_lock_release(arg->lock);
|
|
216
|
+
if (state) rb_jump_tag(state);
|
|
217
|
+
return Qnil;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/* Marks the lock held for the whole body, reads included. A read runs Ruby it
|
|
221
|
+
* does not control -- a key's #hash, a value's #inspect -- and that code reaching
|
|
222
|
+
* back into this object used to wait for a lock its own frame was holding. With
|
|
223
|
+
* the marker set it is reentrant instead, and reaching for a different lock
|
|
224
|
+
* raises rather than inverting the order. */
|
|
225
|
+
static VALUE
|
|
226
|
+
rs_guard_body(VALUE ptr)
|
|
227
|
+
{
|
|
228
|
+
struct rs_guard_arg *arg = (struct rs_guard_arg *)ptr;
|
|
229
|
+
|
|
230
|
+
rs_held_set(arg->thread, arg->self);
|
|
231
|
+
return arg->body(ptr);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
VALUE
|
|
235
|
+
rs_guarded(VALUE self, struct rs_lock *lk, VALUE (*body)(VALUE), void *data,
|
|
236
|
+
bool reentrant, const char *self_hint)
|
|
237
|
+
{
|
|
238
|
+
struct rs_guard_arg arg;
|
|
239
|
+
|
|
240
|
+
arg.self = self;
|
|
241
|
+
arg.thread = rb_thread_current();
|
|
242
|
+
arg.prev_held = rs_held(arg.thread);
|
|
243
|
+
arg.lock = lk;
|
|
244
|
+
arg.data = data;
|
|
245
|
+
|
|
246
|
+
if (arg.prev_held == self) {
|
|
247
|
+
if (reentrant) return body((VALUE)&arg); /* already marked, already held */
|
|
248
|
+
rb_raise(rb_eRactorNestedLock, "already inside this %"PRIsVALUE"; %s",
|
|
249
|
+
rb_obj_class(self), self_hint);
|
|
250
|
+
}
|
|
251
|
+
if (!NIL_P(arg.prev_held)) {
|
|
252
|
+
rb_raise(rb_eRactorNestedLock,
|
|
253
|
+
"already inside another %"PRIsVALUE"; "
|
|
254
|
+
"use Ractor::TVar to change several of them together",
|
|
255
|
+
rb_obj_class(arg.prev_held));
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
arg.body = body;
|
|
259
|
+
rs_lock_acquire(lk);
|
|
260
|
+
/* No interrupt can land between the line above and the ensure below, so
|
|
261
|
+
* nothing here may run Ruby: marking the lock held is the body's first act,
|
|
262
|
+
* inside the ensure, not a step before it. */
|
|
263
|
+
return rb_ensure(rs_guard_body, (VALUE)&arg, rs_guard_ensure, (VALUE)&arg);
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
void
|
|
267
|
+
rs_lock_init_class(void)
|
|
268
|
+
{
|
|
269
|
+
id_new = rb_intern("new");
|
|
270
|
+
id_receive = rb_intern("receive");
|
|
271
|
+
id_push = rb_intern("<<");
|
|
272
|
+
id_held = rb_intern("__ractor_sharing_held__");
|
|
273
|
+
sym_wakeup = ID2SYM(rb_intern("wakeup"));
|
|
274
|
+
rb_gc_register_mark_object(sym_wakeup);
|
|
275
|
+
rb_eRactorClosed = rb_const_get(rb_cRactor, rb_intern("ClosedError"));
|
|
276
|
+
rb_gc_register_mark_object(rb_eRactorClosed);
|
|
277
|
+
rb_eRactorNestedLock = rb_define_class_under(rb_cRactor, "NestedLockError", rb_eThreadError);
|
|
278
|
+
rb_gc_register_mark_object(rb_eRactorNestedLock);
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
void
|
|
282
|
+
Init_lock(void)
|
|
283
|
+
{
|
|
284
|
+
rb_ext_ractor_safe(true); /* these methods are safe to call from any Ractor */
|
|
285
|
+
rs_lock_init_class();
|
|
286
|
+
Init_lockvar_class();
|
|
287
|
+
Init_lockhash_class();
|
|
288
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
#ifndef RACTOR_SHARING_LOCK_H
|
|
2
|
+
#define RACTOR_SHARING_LOCK_H
|
|
3
|
+
|
|
4
|
+
#include "ruby/ruby.h"
|
|
5
|
+
#include "ruby/thread_native.h"
|
|
6
|
+
#include "ruby/ractor.h"
|
|
7
|
+
|
|
8
|
+
/* A lock a Ractor can hold while running Ruby code.
|
|
9
|
+
*
|
|
10
|
+
* The native mutex protects the fields below and nothing else: it is held for a
|
|
11
|
+
* few instructions and never across Ruby code, so a waiter can never keep
|
|
12
|
+
* another Ractor from reaching a GC safepoint. A thread that has to wait parks
|
|
13
|
+
* on a Ractor::Port of its own, which goes through the VM scheduler, so it
|
|
14
|
+
* rides the M:N scheduler and stays interruptible.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
struct rs_waiter {
|
|
18
|
+
VALUE port; /* lives on the waiting thread's stack */
|
|
19
|
+
struct rs_waiter *next;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
struct rs_lock {
|
|
23
|
+
bool locked;
|
|
24
|
+
rb_nativethread_lock_t mutex;
|
|
25
|
+
struct rs_waiter *head, *tail; /* FIFO; each waiter unlinks itself */
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
void rs_lock_init(struct rs_lock *lk);
|
|
29
|
+
void rs_lock_destroy(struct rs_lock *lk);
|
|
30
|
+
void rs_lock_acquire(struct rs_lock *lk);
|
|
31
|
+
void rs_lock_release(struct rs_lock *lk);
|
|
32
|
+
|
|
33
|
+
/* Which lock object this thread is inside, or Qnil. */
|
|
34
|
+
VALUE rs_held(VALUE thread);
|
|
35
|
+
void rs_held_set(VALUE thread, VALUE obj);
|
|
36
|
+
|
|
37
|
+
/* Runs body with +self+ held. If this thread is already inside +self+, body
|
|
38
|
+
* runs directly when +reentrant+, and raises otherwise; being inside a
|
|
39
|
+
* different lock object always raises. */
|
|
40
|
+
VALUE rs_guarded(VALUE self, struct rs_lock *lk, VALUE (*body)(VALUE), void *data,
|
|
41
|
+
bool reentrant, const char *self_hint);
|
|
42
|
+
|
|
43
|
+
/* The argument every guarded body receives. */
|
|
44
|
+
struct rs_guard_arg {
|
|
45
|
+
VALUE self;
|
|
46
|
+
VALUE thread;
|
|
47
|
+
VALUE prev_held;
|
|
48
|
+
struct rs_lock *lock;
|
|
49
|
+
void *data;
|
|
50
|
+
VALUE (*body)(VALUE);
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
extern VALUE rb_eRactorNestedLock;
|
|
54
|
+
|
|
55
|
+
void rs_lock_init_class(void);
|
|
56
|
+
void Init_lockvar_class(void);
|
|
57
|
+
void Init_lockhash_class(void);
|
|
58
|
+
|
|
59
|
+
#endif
|