|
15 | 15 | #include "em_task_queue.h"
|
16 | 16 | #include "proxying_notification_state.h"
|
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 a thread-local task queue is |
36 |
| -// generated, we set a flag on that task queue. Each time that task queue is |
37 |
| -// processed, we clear the flag. The proxying queue can only be freed once |
38 |
| -// `em_proxying_queue_destroy` has been called and the notification flags on |
39 |
| -// each of its task queues have been cleared. |
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 clear the notification |
47 |
| -// flag 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 notification flags to be |
54 |
| -// cleared. The zombie list is scanned whenever a new queue is constructed and |
55 |
| -// any of the zombie queues without outstanding notifications are freed. In |
56 |
| -// principle the zombie list could be scanned at any time, but the queue |
57 |
| -// constructor is a nice place to do it because scanning there is sufficient to |
58 |
| -// keep the number of zombie queues from growing without bound; creating a new |
59 |
| -// zombie ultimately requires creating a new queue. |
60 |
| -// |
61 |
| -// ------------------------------- |
62 |
| - |
63 | 18 | struct em_proxying_queue {
|
64 | 19 | // Protects all accesses to em_task_queues, size, and capacity.
|
65 | 20 | pthread_mutex_t mutex;
|
66 | 21 | // `size` task queue pointers stored in an array of size `capacity`.
|
67 | 22 | em_task_queue** task_queues;
|
68 | 23 | int size;
|
69 | 24 | int capacity;
|
70 |
| - // Doubly linked list pointers for the zombie list. |
71 |
| - em_proxying_queue* zombie_prev; |
72 |
| - em_proxying_queue* zombie_next; |
73 | 25 | };
|
74 | 26 |
|
75 | 27 | // The system proxying queue.
|
76 |
| -static em_proxying_queue system_proxying_queue = {.mutex = |
77 |
| - PTHREAD_MUTEX_INITIALIZER, |
78 |
| - .task_queues = NULL, |
79 |
| - .size = 0, |
80 |
| - .capacity = 0, |
81 |
| - .zombie_prev = NULL, |
82 |
| - .zombie_next = NULL}; |
| 28 | +static em_proxying_queue system_proxying_queue = { |
| 29 | + .mutex = PTHREAD_MUTEX_INITIALIZER, |
| 30 | + .task_queues = NULL, |
| 31 | + .size = 0, |
| 32 | + .capacity = 0, |
| 33 | +}; |
83 | 34 |
|
84 | 35 | em_proxying_queue* emscripten_proxy_get_system_queue(void) {
|
85 | 36 | return &system_proxying_queue;
|
86 | 37 | }
|
87 | 38 |
|
88 |
| -// The head of the zombie list. Its mutex protects access to the list and its |
89 |
| -// other fields are not used. |
90 |
| -static em_proxying_queue zombie_list_head = {.mutex = PTHREAD_MUTEX_INITIALIZER, |
91 |
| - .zombie_prev = &zombie_list_head, |
92 |
| - .zombie_next = &zombie_list_head}; |
93 |
| - |
94 |
| -static void em_proxying_queue_free(em_proxying_queue* q) { |
95 |
| - pthread_mutex_destroy(&q->mutex); |
96 |
| - for (int i = 0; i < q->size; i++) { |
97 |
| - em_task_queue_destroy(q->task_queues[i]); |
98 |
| - } |
99 |
| - free(q->task_queues); |
100 |
| - free(q); |
101 |
| -} |
102 |
| - |
103 |
| -// Does not lock `q` because it should only be called after `q` has been |
104 |
| -// destroyed when it would be UB for new work to come in and race to generate a |
105 |
| -// new notification. |
106 |
| -static int has_notification(em_proxying_queue* q) { |
107 |
| - for (int i = 0; i < q->size; i++) { |
108 |
| - if (q->task_queues[i]->notification != NOTIFICATION_NONE) { |
109 |
| - return 1; |
110 |
| - } |
111 |
| - } |
112 |
| - return 0; |
113 |
| -} |
114 |
| - |
115 |
| -static void cull_zombies() { |
116 |
| - pthread_mutex_lock(&zombie_list_head.mutex); |
117 |
| - em_proxying_queue* curr = zombie_list_head.zombie_next; |
118 |
| - while (curr != &zombie_list_head) { |
119 |
| - em_proxying_queue* next = curr->zombie_next; |
120 |
| - if (!has_notification(curr)) { |
121 |
| - // Remove the zombie from the list and free it. |
122 |
| - curr->zombie_prev->zombie_next = curr->zombie_next; |
123 |
| - curr->zombie_next->zombie_prev = curr->zombie_prev; |
124 |
| - em_proxying_queue_free(curr); |
125 |
| - } |
126 |
| - curr = next; |
127 |
| - } |
128 |
| - pthread_mutex_unlock(&zombie_list_head.mutex); |
129 |
| -} |
130 |
| - |
131 | 39 | em_proxying_queue* em_proxying_queue_create(void) {
|
132 |
| - // Free any queue that has been destroyed and is safe to free. |
133 |
| - cull_zombies(); |
134 |
| - |
135 | 40 | // Allocate the new queue.
|
136 | 41 | em_proxying_queue* q = malloc(sizeof(em_proxying_queue));
|
137 | 42 | if (q == NULL) {
|
138 | 43 | return NULL;
|
139 | 44 | }
|
140 |
| - *q = (em_proxying_queue){.mutex = PTHREAD_MUTEX_INITIALIZER, |
141 |
| - .task_queues = NULL, |
142 |
| - .size = 0, |
143 |
| - .capacity = 0, |
144 |
| - .zombie_prev = NULL, |
145 |
| - .zombie_next = NULL}; |
| 45 | + *q = (em_proxying_queue){ |
| 46 | + .mutex = PTHREAD_MUTEX_INITIALIZER, |
| 47 | + .task_queues = NULL, |
| 48 | + .size = 0, |
| 49 | + .capacity = 0, |
| 50 | + }; |
146 | 51 | return q;
|
147 | 52 | }
|
148 | 53 |
|
149 | 54 | void em_proxying_queue_destroy(em_proxying_queue* q) {
|
150 | 55 | assert(q != NULL);
|
151 | 56 | assert(q != &system_proxying_queue && "cannot destroy system proxying queue");
|
152 |
| - assert(!q->zombie_next && !q->zombie_prev && |
153 |
| - "double freeing em_proxying_queue!"); |
154 |
| - if (!has_notification(q)) { |
155 |
| - // No outstanding references to the queue, so we can go ahead and free it. |
156 |
| - em_proxying_queue_free(q); |
157 |
| - return; |
| 57 | + |
| 58 | + pthread_mutex_destroy(&q->mutex); |
| 59 | + for (int i = 0; i < q->size; i++) { |
| 60 | + em_task_queue_destroy(q->task_queues[i]); |
158 | 61 | }
|
159 |
| - // Otherwise add the queue to the zombie list so that it will eventually be |
160 |
| - // freed safely. |
161 |
| - pthread_mutex_lock(&zombie_list_head.mutex); |
162 |
| - q->zombie_next = zombie_list_head.zombie_next; |
163 |
| - q->zombie_prev = &zombie_list_head; |
164 |
| - q->zombie_next->zombie_prev = q; |
165 |
| - q->zombie_prev->zombie_next = q; |
166 |
| - pthread_mutex_unlock(&zombie_list_head.mutex); |
| 62 | + free(q->task_queues); |
| 63 | + free(q); |
167 | 64 | }
|
168 | 65 |
|
169 | 66 | // Not thread safe. Returns NULL if there are no tasks for the thread.
|
|
0 commit comments