naev 0.12.6
threadpool.c
1/*
2 * See Licensing and Copyright notice in threadpool.h
3 */
4/*
5 * @brief A simple threadpool implementation using a single queue.
6 *
7 * The queue is inspired by this paper (look for the queue with two locks):
8 *
9 * Maged M. Michael and Michael L. Scott. 1998. Nonblocking algorithms and
10 * preemption-safe locking on multiprogrammed shared memory multiprocessors. J.
11 * Parallel Distrib. Comput. 51, 1 (May 1998), 1-26. DOI=10.1006/jpdc.1998.1446
12 * http://dx.doi.org/10.1006/jpdc.1998.1446
13 *
14 * @ARTICLE{Michael98non-blockingalgorithms,
15 * author = {Maged M. Michael and Michael L. Scott},
16 * title = {Non-Blocking Algorithms and Preemption-Safe Locking on
17 * Multiprogrammed Shared Memory Multiprocessors}, journal = {Journal of
18 * Parallel and Distributed Computing}, year = {1998}, volume = {51}, pages =
19 * {1--26},
20 * }
21 *
22 * @note The algorithm/strategy for killing idle workers should be moved into
23 * the threadhandler and it should also be improved (the current strategy
24 * is probably not very good).
25 */
26
28#include <stdlib.h>
29
30#include "SDL_cpuinfo.h"
31#include "SDL_thread.h"
33
34#include "threadpool.h"
35
36#include "array.h"
37#include "log.h"
38
39#define THREADPOOL_TIMEOUT \
40 ( 5 * 100 ) /* The time a worker thread waits in ms. */
41#define THREADSIG_STOP ( 1 ) /* The signal to stop a worker thread */
42#define THREADSIG_RUN \
43 ( 0 ) /* The signal to indicate the worker thread is running */
44
48static int MAXTHREADS = 8; /* Bit overkill, but oh well. */
49
53typedef struct Node_ {
54 void *data;
55 struct Node_ *next;
56} Node;
57
58struct vpoolThreadData_;
59
67 /* A semaphore to ensure reads only happen when the queue is not empty */
68 SDL_sem *semaphore;
69 SDL_mutex *t_lock;
70 SDL_mutex *h_lock;
71 SDL_mutex *r_lock;
72 /* For vpools. */
73 SDL_cond *cond;
74 SDL_mutex *mutex;
75 struct vpoolThreadData_ *arg;
76 int cnt;
77};
78
82typedef struct ThreadQueueData_ {
83 int ( *function )( void * ); /* The function to be called */
84 void *data; /* And its arguments */
86
90typedef struct ThreadData_ {
91 int ( *function )( void * ); /* The function to be called */
92 void *data; /* Arguments to the above function */
93 int signal; /* Signals to the thread */
94 SDL_sem *semaphore; /* The semaphore to signal new jobs or new signal in the
95 'signal' variable */
96 ThreadQueue *idle; /* The queue with idle threads */
97 ThreadQueue *stopped; /* The queue with stopped threads */
99
111typedef struct vpoolThreadData_ vpoolThreadData;
112
113/* The global threadpool queue */
114static ThreadQueue *global_queue = NULL;
115
116/*
117 * Prototypes.
118 */
119static ThreadQueue *tq_create( void );
120static void tq_enqueue( ThreadQueue *q, void *data );
121static void *tq_dequeue( ThreadQueue *q );
122static void tq_destroy( ThreadQueue *q );
123static int threadpool_worker( void *data );
124static int threadpool_handler( void *data );
125static int vpool_worker( void *data );
126
136static ThreadQueue *tq_create( void )
137{
138 ThreadQueue *q;
139 Node *n;
140
141 /* Queue memory allocation. */
142 q = calloc( 1, sizeof( ThreadQueue ) );
143
144 /* Allocate and insert the dummy node */
145 n = calloc( 1, sizeof( Node ) );
146 n->next = NULL;
147 q->first = n;
148 q->last = n;
149
150 /* Create locks. */
151 q->t_lock = SDL_CreateMutex();
152 q->h_lock = SDL_CreateMutex();
153 q->r_lock = SDL_CreateMutex();
154 q->semaphore = SDL_CreateSemaphore( 0 );
155
156 return q;
157}
158
165static void tq_enqueue( ThreadQueue *q, void *data )
166{
167 Node *n;
168
169 /* Try to grab reserved struct if possible. */
170 SDL_mutexP( q->r_lock );
171 if ( q->reserve != NULL ) {
172 n = q->reserve;
173 q->reserve = n->next;
174 } else
175 n = malloc( sizeof( Node ) );
176 n->data = data;
177 n->next = NULL;
178 SDL_mutexV( q->r_lock );
179
180 /* Lock */
181 SDL_mutexP( q->t_lock );
182
183 /* Enqueue. */
184 q->last->next = n;
185 q->last = n;
186
187 /* Signal and unlock. This wil break if someone tries to enqueue 2^32+1
188 * elements or something. */
189 SDL_SemPost( q->semaphore );
190 SDL_mutexV( q->t_lock );
191}
192
202static void *tq_dequeue( ThreadQueue *q )
203{
204 void *d;
205 Node *newhead, *node;
206
207 /* Lock the head. */
208 SDL_mutexP( q->h_lock );
209
210 /* Start running. */
211 node = q->first;
212 newhead = node->next;
213
214 /* Head not consistent. */
215 if ( newhead == NULL ) {
216 WARN( _( "Tried to dequeue while the queue was empty!" ) );
217 /* Ugly fix :/ */
218 /*
219 SDL_mutexV(q->h_lock);
220 return NULL;
221 */
222 /* We prefer to wait until the cache updates :/ */
223 do {
224 node = q->first;
225 newhead = node->next;
226 } while ( newhead == NULL );
227 }
228
229 /* Remember the value and assign newhead as the new dummy element. */
230 d = newhead->data;
231 q->first = newhead;
232
233 /* Unlock */
234 SDL_mutexV( q->h_lock );
235
236 /* Save memory in reserve. */
237 SDL_mutexP( q->r_lock );
238 node->next = q->reserve;
239 q->reserve = node;
240 SDL_mutexV( q->r_lock );
241
242 return d;
243}
244
252static void tq_destroy( ThreadQueue *q )
253{
254 /* Iterate through the list and free the nodes */
255 while ( q->first != NULL ) {
256 Node *n = q->first;
257 q->first = n->next;
258 free( n );
259 }
260
261 /* Free reserve. */
262 while ( q->reserve != NULL ) {
263 Node *n = q->reserve;
264 q->reserve = n->next;
265 free( n );
266 }
267
268 /* Clean up threading structures. */
269 SDL_DestroySemaphore( q->semaphore );
270 SDL_DestroyMutex( q->h_lock );
271 SDL_DestroyMutex( q->t_lock );
272 SDL_DestroyMutex( q->r_lock );
273
274 /* Clean up vpool structures. */
275 if ( q->mutex != NULL )
276 SDL_DestroyMutex( q->mutex );
277 if ( q->cond != NULL )
278 SDL_DestroyCond( q->cond );
279 array_free( q->arg );
280
281 free( q->first );
282 free( q );
283}
284
294static int threadpool_worker( void *data )
295{
296 ThreadData *work = (ThreadData *)data;
297
298 /* Work loop */
299 while ( 1 ) {
300 /* Wait for new signal */
301 while ( SDL_SemWait( work->semaphore ) == -1 ) {
302 /* Putting this in a while-loop is probably a really bad idea, but I
303 * don't have any better ideas. */
304 WARN( _( "SDL_SemWait failed! Error: %s" ), SDL_GetError() );
305 }
306 /* Break if received signal to stop */
307 if ( work->signal == THREADSIG_STOP )
308 break;
309
310 /* Do work :-) */
311 work->function( work->data );
312
313 /* Enqueue itself in the idle worker threads queue */
314 tq_enqueue( work->idle, work );
315 }
316 /* Enqueue itself in the stopped worker threads queue when stopped */
317 tq_enqueue( work->stopped, work );
318
319 return 0;
320}
321
342static int threadpool_handler( void *data )
343{
344 (void)data;
345 int nrunning, newthread;
346 ThreadData *threadargs, *threadarg;
347 /* Queues for idle workers and stopped workers */
348 ThreadQueue *idle, *stopped;
350
351 /* Initialize the idle and stopped queues. */
352 idle = tq_create();
353 stopped = tq_create();
354
355 /* Allocate threadargs to communicate with workers */
356 threadargs = calloc( MAXTHREADS, sizeof( ThreadData ) );
357
358 /* Initialize threadargs */
359 for ( int i = 0; i < MAXTHREADS; i++ ) {
360 threadargs[i].function = NULL;
361 threadargs[i].data = NULL;
362 threadargs[i].semaphore =
363 SDL_CreateSemaphore( 0 ); /* Used to give orders. */
364 threadargs[i].idle = idle;
365 threadargs[i].stopped = stopped;
366 threadargs[i].signal = THREADSIG_RUN;
367 /* 'Workers' that do not have a thread are considered stopped */
368 tq_enqueue( stopped, &threadargs[i] );
369 }
370
371 /* Set the number of running threads to 0 */
372 nrunning = 0;
373
374 /*
375 * Thread handler main loop.
376 */
377 while ( 1 ) {
378 /*
379 * We must now wait, this shall be done on each active thread. However
380 * they will be put to sleep as time passes. When we receive a command
381 * we'll proceed to process it.
382 */
383 if ( nrunning > 0 ) {
384 /*
385 * Here we'll wait until thread gets work to do. If it doesn't it will
386 * just stop a worker thread and wait until it gets something to do.
387 */
388 if ( SDL_SemWaitTimeout( global_queue->semaphore,
389 THREADPOOL_TIMEOUT ) != 0 ) {
390 /* There weren't any new jobs so we'll start killing threads ;) */
391 if ( SDL_SemTryWait( idle->semaphore ) == 0 ) {
392 threadarg = tq_dequeue( idle );
393 /* Set signal to stop worker thread */
394 threadarg->signal = THREADSIG_STOP;
395 /* Signal thread and decrement running threads counter */
396 SDL_SemPost( threadarg->semaphore );
397 nrunning -= 1;
398 }
399
400 /* We just go back to waiting on a thread. */
401 continue;
402 }
403
404 /* We got work. Continue to handle work. */
405 } else {
406 /*
407 * Here we wait for a new job. No threads are alive at this point and
408 * the threadpool is just patiently waiting for work to arrive.
409 */
410 if ( SDL_SemWait( global_queue->semaphore ) == -1 ) {
411 WARN( _( "SDL_SemWait failed! Error: %s" ), SDL_GetError() );
412 continue;
413 }
414
415 /* We got work. Continue to handle work. */
416 }
417
418 /*
419 * Get a new job from the queue. This should be safe as we have received
420 * a permission from the global_queue->semaphore.
421 */
422 node = tq_dequeue( global_queue );
423 newthread = 0;
424
425 /*
426 * Choose where to get the thread. Either idle, revive stopped or block
427 * until another thread becomes idle.
428 */
429 /* Idle thread available */
430 if ( SDL_SemTryWait( idle->semaphore ) == 0 )
431 threadarg = tq_dequeue( idle );
432 /* Make a new thread */
433 else if ( SDL_SemTryWait( stopped->semaphore ) == 0 ) {
434 threadarg = tq_dequeue( stopped );
435 threadarg->signal = THREADSIG_RUN;
436 newthread = 1;
437 }
438 /* Wait for idle thread */
439 else {
440 while ( SDL_SemWait( idle->semaphore ) == -1 ) {
441 /* Bad idea */
442 WARN( _( "SDL_SemWait failed! Error: %s" ), SDL_GetError() );
443 }
444 threadarg = tq_dequeue( idle );
445 }
446
447 /* Assign arguments for the thread */
448 threadarg->function = node->function;
449 threadarg->data = node->data;
450 /* Signal the thread that there's a new job */
451 SDL_SemPost( threadarg->semaphore );
452
453 /* Start a new thread and increment the thread counter */
454 if ( newthread ) {
455 SDL_CreateThread( threadpool_worker, "threadpool_worker", threadarg );
456 nrunning += 1;
457 }
458 }
460
461 /* Clean up. */
462 tq_destroy( idle );
463 tq_destroy( stopped );
464 free( threadargs );
465
466 return 0;
467}
468
474int threadpool_init( void )
475{
476 MAXTHREADS = SDL_GetCPUCount() + 1; /* SDL 1.3 is pretty cool. */
477
478 /* There's already a queue */
479 if ( global_queue != NULL ) {
480 WARN( _( "Threadpool has already been initialized!" ) );
481 return -1;
482 }
483
484 /* Create the global queue queue */
485 global_queue = tq_create();
486
487 /* Initialize the threadpool handler. */
488 if ( SDL_CreateThread( threadpool_handler, "threadpool_handler", NULL ) ==
489 NULL ) {
490 ERR( _( "Threadpool init failed: %s" ), SDL_GetError() );
491 return -1;
492 }
493
494 return 0;
495}
496
511ThreadQueue *vpool_create( void )
512{
513 ThreadQueue *tq = tq_create();
514 /* Create vpool-specific threading structures. */
515 tq->cond = SDL_CreateCond();
516 tq->mutex = SDL_CreateMutex();
517 tq->arg = array_create( vpoolThreadData );
518 return tq;
519}
520
527void vpool_enqueue( ThreadQueue *queue, int ( *function )( void * ),
528 void *data )
529{
530 vpoolThreadData *arg = &array_grow( &queue->arg );
531 memset( arg, 0, sizeof( vpoolThreadData ) );
532 /* Common field.s */
533 arg->cond = queue->cond;
534 arg->mutex = queue->mutex;
535 arg->count = &queue->cnt;
536 /* Task-specific stuff. */
537 arg->node.data = data;
538 arg->node.function = function;
539 SDL_SemPost( queue->semaphore );
540 arg->wrapper.function = vpool_worker;
541}
542
549static int vpool_worker( void *data )
550{
551 int cnt;
552 vpoolThreadData *work = (vpoolThreadData *)data;
553
554 /* Do work */
555 work->node.function( work->node.data );
556
557 /* Decrement the counter and signal vpool_wait if all threads are done */
558 SDL_mutexP( work->mutex );
559 cnt = *( work->count ) - 1;
560 if ( cnt <= 0 ) /* All jobs done. */
561 SDL_CondSignal( work->cond ); /* Signal waiting thread */
562 *( work->count ) = cnt;
563 SDL_mutexV( work->mutex );
564
565 return 0;
566}
567
568/* @brief Run every job in the vpool queue and block until every job in the
569 * queue is done.
570 *
571 * @note It destroys the queue when it's done.
572 */
573void vpool_wait( ThreadQueue *queue )
574{
575 /* Number of tasks we have. */
576 int cnt;
577 queue->cnt = array_size( queue->arg );
578 cnt = queue->cnt;
579
580 if ( global_queue == NULL ) {
581 WARN( _( "Threadpool has not been initialized yet!" ) );
582 return;
583 }
584
585 /* Nothing to do. */
586 if ( cnt <= 0 )
587 return;
588
589 /* Allocate all vpoolThreadData objects */
590 SDL_mutexP( queue->mutex );
591 /* Initialize the vpoolThreadData */
592 for ( int i = 0; i < cnt; i++ ) {
593 vpoolThreadData *arg;
594 /* This is needed to keep the invariants of the queue */
595 while ( SDL_SemWait( queue->semaphore ) == -1 ) {
596 /* Again, a really bad idea */
597 WARN( _( "SDL_SemWait failed! Error: %s" ), SDL_GetError() );
598 }
599 /* Launch new job. */
600 arg = &queue->arg[i];
601 arg->wrapper.data = arg;
602 tq_enqueue( global_queue, &queue->arg[i].wrapper );
603 }
604
605 /* Wait for the threads to finish */
606 SDL_CondWait( queue->cond, queue->mutex );
607 SDL_mutexV( queue->mutex );
608
609 /* Can toss away all the queue stuff. */
610 array_erase( &queue->arg, array_begin( queue->arg ),
611 array_end( queue->arg ) );
612}
613
617void vpool_cleanup( ThreadQueue *queue )
618{
619 /* Clean up */
620 tq_destroy( queue );
621}
Provides macros to work with dynamic arrays.
#define array_free(ptr_array)
Frees memory allocated and sets array to NULL.
Definition array.h:170
#define array_end(array)
Returns a pointer to the end of the reserved memory space.
Definition array.h:214
#define array_erase(ptr_array, first, last)
Erases elements in interval [first, last).
Definition array.h:148
static ALWAYS_INLINE int array_size(const void *array)
Returns number of elements in the array.
Definition array.h:179
#define array_grow(ptr_array)
Increases the number of elements by one and returns the last element.
Definition array.h:122
#define array_begin(array)
Returns a pointer to the beginning of the reserved memory space.
Definition array.h:206
#define array_create(basic_type)
Creates a new dynamic array of ‘basic_type’.
Definition array.h:93
static const double d[]
Definition rng.c:263
Node struct.
Definition queue.c:22
Represents a node of an object. Each node can have multiple meshes and children nodes with an associa...
Definition gltf.h:105
void * data
Definition threadpool.c:54
struct Node_ * next
Definition threadpool.c:55
Thread data.
Definition threadpool.c:90
Data for the threadqueue.
Definition threadpool.c:82
Threadqueue itself.
Definition threadpool.c:63
SDL_mutex * r_lock
Definition threadpool.c:71
Node * first
Definition threadpool.c:64
Node * last
Definition threadpool.c:65
Node * reserve
Definition threadpool.c:66
SDL_mutex * h_lock
Definition threadpool.c:70
SDL_mutex * t_lock
Definition threadpool.c:69
Virtual thread pool data.
Definition threadpool.c:103
ThreadQueueData wrapper
Definition threadpool.c:109
SDL_cond * cond
Definition threadpool.c:104
SDL_mutex * mutex
Definition threadpool.c:106
ThreadQueueData node
Definition threadpool.c:108