15
15
16
16
#define TASK_QUEUE_INITIAL_CAPACITY 128
17
17
18
+ // Proxy Queue Lifetime Management
19
+ // -------------------------------
20
+ //
21
+ // Proxied tasks are executed either when the user manually calls
22
+ // `emscripten_proxy_execute_queue` on the target thread or when the target
23
+ // thread returns to the event loop. The queue does not know which execution
24
+ // path will be used ahead of time when the work is proxied, so it must
25
+ // conservatively send a message to the target thread's event loop in case the
26
+ // user expects the event loop to drive the execution. These notifications
27
+ // contain references to the queue that will be dereferenced when the target
28
+ // thread returns to its event loop and receives the notification, even if the
29
+ // user manages the execution of the queue themselves.
30
+ //
31
+ // To avoid use-after-free bugs, we cannot free a queue immediately when a user
32
+ // calls `em_proxying_queue_destroy`; instead, we have to defer freeing the
33
+ // queue until all of its outstanding notifications have been processed. We
34
+ // defer freeing the queue using a reference counting scheme. Each time a
35
+ // notification containing a reference to the queue is generated, we increase
36
+ // the reference count and each time one of the notifications is received and
37
+ // processed, we decrease the reference count. The queue can only be freed once
38
+ // `em_proxying_queue_destroy` has been called and the reference count has
39
+ // reached zero.
40
+ //
41
+ // But an extra complication is that the target thread may have died by the time
42
+ // it gets back to its event loop to process its notifications. This can happen
43
+ // when a user proxies some work to a thread, then calls
44
+ // `emscripten_proxy_execute_queue` on that thread, then destroys the queue and
45
+ // exits the thread. In that situation no work will be dropped, but the thread's
46
+ // worker will still receive a notification and have to decrease the reference
47
+ // count without a live runtime. Without a live runtime, there is no stack, so
48
+ // the worker cannot safely free the queue at this point even if the refcount
49
+ // goes to zero. We need a separate thread with a live runtime to perform the
50
+ // free.
51
+ //
52
+ // To ensure that queues are eventually freed, we place destroyed queues in a
53
+ // global "zombie list" where they wait for their refcounts to fall to zero. The
54
+ // zombie list is scanned whenever a new queue is constructed and any of the
55
+ // zombie queues with zero refcounts are freed. In principle the zombie list
56
+ // could be scanned at any time, but the queue constructor is a nice place to do
57
+ // it because scanning there is sufficient to keep the number of zombie queues
58
+ // from growing without bound; creating a new zombie ultimately requires
59
+ // creating a new queue.
60
+
18
61
extern int _emscripten_notify_proxying_queue (pthread_t target_thread ,
19
62
pthread_t curr_thread ,
20
63
pthread_t main_thread ,
@@ -120,15 +163,25 @@ static task task_queue_dequeue(task_queue* tasks) {
120
163
}
121
164
122
165
struct em_proxying_queue {
123
- // Protects all accesses to all task_queues.
166
+ // The number of references to this queue that exist in JS event queues.
167
+ // Decremented directly from JS, so this must be the first field.
168
+ _Atomic int js_refcount ;
169
+ // Doubly linked list pointers for the zombie list.
170
+ em_proxying_queue * zombie_prev ;
171
+ em_proxying_queue * zombie_next ;
172
+ // Protects all accesses to task_queues, size, and capacity.
124
173
pthread_mutex_t mutex ;
125
174
// `size` task queues stored in an array of size `capacity`.
126
175
task_queue * task_queues ;
127
176
int size ;
128
177
int capacity ;
129
178
};
130
179
131
- static em_proxying_queue system_proxying_queue = {.mutex =
180
+ // The system proxying queue.
181
+ static em_proxying_queue system_proxying_queue = {.js_refcount = 0 ,
182
+ .zombie_prev = NULL ,
183
+ .zombie_next = NULL ,
184
+ .mutex =
132
185
PTHREAD_MUTEX_INITIALIZER ,
133
186
.task_queues = NULL ,
134
187
.size = 0 ,
@@ -138,12 +191,51 @@ em_proxying_queue* emscripten_proxy_get_system_queue(void) {
138
191
return & system_proxying_queue ;
139
192
}
140
193
194
+ // The head of the zombie list. Its mutex protects access to the list and its
195
+ // other fields are not used..
196
+ static em_proxying_queue zombie_list_head = {.zombie_prev = & zombie_list_head ,
197
+ .zombie_next = & zombie_list_head ,
198
+ .mutex =
199
+ PTHREAD_MUTEX_INITIALIZER };
200
+
201
+ static void em_proxying_queue_free (em_proxying_queue * q ) {
202
+ pthread_mutex_destroy (& q -> mutex );
203
+ for (int i = 0 ; i < q -> size ; i ++ ) {
204
+ task_queue_deinit (& q -> task_queues [i ]);
205
+ }
206
+ free (q -> task_queues );
207
+ free (q );
208
+ }
209
+
210
+ static void cull_zombies () {
211
+ pthread_mutex_lock (& zombie_list_head .mutex );
212
+ em_proxying_queue * curr = zombie_list_head .zombie_next ;
213
+ while (curr != & zombie_list_head ) {
214
+ em_proxying_queue * next = curr -> zombie_next ;
215
+ if (curr -> js_refcount == 0 ) {
216
+ // Remove the zombie from the list and free it.
217
+ curr -> zombie_prev -> zombie_next = curr -> zombie_next ;
218
+ curr -> zombie_next -> zombie_prev = curr -> zombie_prev ;
219
+ em_proxying_queue_free (curr );
220
+ }
221
+ curr = next ;
222
+ }
223
+ pthread_mutex_unlock (& zombie_list_head .mutex );
224
+ }
225
+
141
226
em_proxying_queue * em_proxying_queue_create (void ) {
227
+ // Free any queue that has been destroyed and is safe to free.
228
+ cull_zombies ();
229
+
230
+ // Allocate the new queue.
142
231
em_proxying_queue * q = malloc (sizeof (em_proxying_queue ));
143
232
if (q == NULL ) {
144
233
return NULL ;
145
234
}
146
- * q = (em_proxying_queue ){.mutex = PTHREAD_MUTEX_INITIALIZER ,
235
+ * q = (em_proxying_queue ){.js_refcount = 0 ,
236
+ .zombie_prev = NULL ,
237
+ .zombie_next = NULL ,
238
+ .mutex = PTHREAD_MUTEX_INITIALIZER ,
147
239
.task_queues = NULL ,
148
240
.size = 0 ,
149
241
.capacity = 0 };
@@ -153,14 +245,21 @@ em_proxying_queue* em_proxying_queue_create(void) {
153
245
void em_proxying_queue_destroy (em_proxying_queue * q ) {
154
246
assert (q != NULL );
155
247
assert (q != & system_proxying_queue && "cannot destroy system proxying queue" );
156
- // No need to acquire the lock; no one should be racing with the destruction
157
- // of the queue.
158
- pthread_mutex_destroy (& q -> mutex );
159
- for (int i = 0 ; i < q -> size ; i ++ ) {
160
- task_queue_deinit (& q -> task_queues [i ]);
248
+ assert (!q -> zombie_next && !q -> zombie_prev &&
249
+ "double freeing em_proxying_queue!" );
250
+ if (q -> js_refcount == 0 ) {
251
+ // No outstanding references to the queue, so we can go ahead and free it.
252
+ em_proxying_queue_free (q );
253
+ return ;
161
254
}
162
- free (q -> task_queues );
163
- free (q );
255
+ // Otherwise add the queue to the zombie list so that it will eventually be
256
+ // freed safely.
257
+ pthread_mutex_lock (& zombie_list_head .mutex );
258
+ q -> zombie_next = zombie_list_head .zombie_next ;
259
+ q -> zombie_prev = & zombie_list_head ;
260
+ q -> zombie_next -> zombie_prev = q ;
261
+ q -> zombie_prev -> zombie_next = q ;
262
+ pthread_mutex_unlock (& zombie_list_head .mutex );
164
263
}
165
264
166
265
// Not thread safe. Returns -1 if there are no tasks for the thread.
@@ -268,6 +367,7 @@ int emscripten_proxy_async(em_proxying_queue* q,
268
367
// Otherwise, the target thread was already notified when the existing work
269
368
// was enqueued so we don't need to notify it again.
270
369
if (empty ) {
370
+ q -> js_refcount ++ ;
271
371
_emscripten_notify_proxying_queue (
272
372
target_thread , pthread_self (), emscripten_main_browser_thread_id (), q );
273
373
}
0 commit comments