Ruby 4.1.0dev (2026-09-27 revision f6ff9e7d02e46360f8930b280a3dd921cccbda29)
Context.c
1/*
2 * This file is part of the "Coroutine" project and released under the MIT License.
3 *
4 * Created by Samuel Williams on 24/6/2021.
5 * Copyright, 2021, by Samuel Williams.
6*/
7
8#include "Context.h"
9#include <stdio.h>
10#include <errno.h>
11
12static const int DEBUG = 0;
13
14static
15int check(const char * message, int result) {
16 if (result) {
17 switch (result) {
18 case EDEADLK:
19 if (DEBUG) fprintf(stderr, "deadlock detected result=%d errno=%d\n", result, errno);
20 break;
21 default:
22 if (DEBUG) fprintf(stderr, "error detected result=%d errno=%d\n", result, errno);
23 perror(message);
24 }
25 }
26
27 assert(result == 0);
28
29 return result;
30}
31
32void coroutine_initialize_main(struct coroutine_context * context) {
33 context->id = pthread_self();
34 context->start = NULL;
35
36 check("coroutine_initialize_main:pthread_mutex_init",
37 pthread_mutex_init(&context->guard, NULL)
38 );
39
40 check("coroutine_initialize_main:pthread_cond_init",
41 pthread_cond_init(&context->schedule, NULL)
42 );
43
44 context->suspended = 0;
45 context->initialized = 1;
46 context->thread_created = 0;
47 context->from = NULL;
48}
49
50void coroutine_initialize(
51 struct coroutine_context *context,
52 coroutine_start start,
53 void *stack,
54 size_t size
55) {
56 assert(start && stack && size >= 1024);
57
58 // We will create the thread when we first transfer, but save the details now:
59 context->start = start;
60 context->stack = stack;
61 context->size = size;
62
63 check("coroutine_initialize:pthread_mutex_init",
64 pthread_mutex_init(&context->guard, NULL)
65 );
66
67 check("coroutine_initialize:pthread_cond_init",
68 pthread_cond_init(&context->schedule, NULL)
69 );
70
71 /* A worker is initially resumable even though its pthread is created
72 * lazily by the first transfer. */
73 context->suspended = 1;
74 context->initialized = 1;
75 context->thread_created = 0;
76 context->from = NULL;
77}
78
79static
80int is_locked(pthread_mutex_t * mutex) {
81 int result = pthread_mutex_trylock(mutex);
82
83 // If we could successfully lock the mutex:
84 if (result == 0) {
85 pthread_mutex_unlock(mutex);
86 // We could lock the mutex, so it wasn't locked:
87 return 0;
88 } else {
89 // Otherwise we couldn't lock it because it's already locked:
90 return 1;
91 }
92}
93
94static
95void coroutine_guard_unlock(void * _context)
96{
97 struct coroutine_context * context = _context;
98
99 if (DEBUG) fprintf(stderr, "coroutine_guard_unlock:pthread_mutex_unlock\n");
100
101 check("coroutine_guard_unlock:pthread_mutex_unlock",
102 pthread_mutex_unlock(&context->guard)
103 );
104}
105
106void * coroutine_trampoline(void * _context)
107{
108 struct coroutine_context * context = _context;
109
110 context->start(context->from, context);
111
112 return NULL;
113}
114
115static
116int coroutine_create_thread(struct coroutine_context *context)
117{
118 int result;
119
120 pthread_attr_t attr;
121 result = pthread_attr_init(&attr);
122 if (result != 0) {
123 return result;
124 }
125
126 result = pthread_attr_setstack(&attr, context->stack, (size_t)context->size);
127 if (result != 0) {
128 pthread_attr_destroy(&attr);
129 return result;
130 }
131
132 result = pthread_create(&context->id, &attr, coroutine_trampoline, context);
133 pthread_attr_destroy(&attr);
134
135 if (result != 0) {
136 return result;
137 }
138
139 context->thread_created = 1;
140
141 return result;
142}
143
144static
145void coroutine_lock_pair(struct coroutine_context *current, struct coroutine_context *target)
146{
147 /* A valid transfer targets a suspended context, so it cannot be trying to
148 * acquire current->guard while we acquire target->guard. */
149 check("coroutine_transfer:pthread_mutex_lock(current)",
150 pthread_mutex_lock(&current->guard)
151 );
152
153 check("coroutine_transfer:pthread_mutex_lock(target)",
154 pthread_mutex_lock(&target->guard)
155 );
156}
157
158static
159void coroutine_unlock_pair(struct coroutine_context *current, struct coroutine_context *target)
160{
161 check("coroutine_transfer:pthread_mutex_unlock(target)",
162 pthread_mutex_unlock(&target->guard)
163 );
164
165 check("coroutine_transfer:pthread_mutex_unlock(current)",
166 pthread_mutex_unlock(&current->guard)
167 );
168}
169
170struct coroutine_context * coroutine_transfer(struct coroutine_context * current, struct coroutine_context * target)
171{
172 assert(current->initialized);
173 assert(target->initialized);
174 assert(current != target);
175
176 int result = 0;
177
178 coroutine_lock_pair(current, target);
179
180 if (current->start == NULL) {
181 /* A main context follows its caller, which may change when Ruby's M:N
182 * scheduler moves a Ruby thread to another native thread. */
183 current->id = pthread_self();
184 }
185 else {
186 assert(current->thread_created);
187 assert(pthread_equal(current->id, pthread_self()));
188 }
189 assert(!current->suspended);
190 assert(target->suspended);
191
192 struct coroutine_context * previous = target->from;
193
194 current->suspended = 1;
195 target->suspended = 0;
196 target->from = current;
197
198 // First transfer:
199 if (target->start != NULL && !target->thread_created) {
200 if (DEBUG) fprintf(stderr, "coroutine_transfer:coroutine_create_thread...\n");
201 result = coroutine_create_thread(target);
202 if (result != 0) {
203 if (DEBUG) fprintf(stderr, "coroutine_transfer:coroutine_create_thread failed\n");
204 }
205 } else {
206 if (DEBUG) fprintf(stderr, "coroutine_transfer:pthread_cond_signal(target)\n");
207 result = pthread_cond_signal(&target->schedule);
208 }
209
210 if (result != 0) {
211 target->from = previous;
212 target->suspended = 1;
213 current->suspended = 0;
214 coroutine_unlock_pair(current, target);
215 errno = result;
216 return NULL;
217 }
218
219 check("coroutine_transfer:pthread_mutex_unlock(target)",
220 pthread_mutex_unlock(&target->guard)
221 );
222
223 pthread_cleanup_push(coroutine_guard_unlock, current);
224
225 while (current->suspended) {
226 // A side effect of acting upon a cancellation request while in a condition wait is that the mutex is (in effect) re-acquired before calling the first cancellation cleanup handler. If cancelled, pthread_cond_wait immediately invokes cleanup handlers.
227 if (DEBUG) fprintf(stderr, "coroutine_transfer:pthread_cond_wait(schedule=%p, guard=%p, is_locked=%d)\n", &current->schedule, &current->guard, is_locked(&current->guard));
228 check("coroutine_transfer:pthread_cond_wait",
229 pthread_cond_wait(&current->schedule, &current->guard)
230 );
231 }
232
233 if (DEBUG) fprintf(stderr, "coroutine_transfer:pthread_cleanup_pop\n");
234 pthread_cleanup_pop(1);
235
236#ifdef __FreeBSD__
237 // Apparently required for FreeBSD:
238 pthread_testcancel();
239#endif
240
241 /* current may have been resumed by a context other than target. */
242 return current->from;
243}
244
245static
246void coroutine_join(struct coroutine_context * context) {
247 if (DEBUG) fprintf(stderr, "coroutine_join:pthread_cancel\n");
248 int result = pthread_cancel(context->id);
249 if (result == -1 && errno == ESRCH) {
250 // The thread may be dead due to fork, so it cannot be joined and this doesn't represent a real error:
251 return;
252 }
253
254 check("coroutine_join:pthread_cancel", result);
255
256 if (DEBUG) fprintf(stderr, "coroutine_join:pthread_join\n");
257 check("coroutine_join:pthread_join",
258 pthread_join(context->id, NULL)
259 );
260
261 if (DEBUG) fprintf(stderr, "coroutine_join:pthread_join done\n");
262}
263
264void coroutine_destroy(struct coroutine_context * context)
265{
266 if (DEBUG) fprintf(stderr, "coroutine_destroy\n");
267
268 assert(context);
269
270 if (!context->initialized) return;
271
272 if (context->thread_created) {
273 coroutine_join(context);
274 context->thread_created = 0;
275 }
276
277 if (DEBUG) fprintf(stderr, "coroutine_destroy:pthread_cond_destroy(%p)\n", &context->schedule);
278 pthread_cond_destroy(&context->schedule);
279 pthread_mutex_destroy(&context->guard);
280 context->initialized = 0;
281}
#define errno
Ractor-aware version of errno.
Definition ruby.h:388