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.
@@ -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