ascii-chat
Binary v0.15.83-53-gb711d8720
Library lib/v0.3.2-701-gb711d8720
Video chat in your terminal
Loading...
Searching...
No Matches
thread_pool.h File Reference

🧵 Generic thread pool abstraction for managing worker threads More...

Go to the source code of this file.

Data Structures

struct  thread_pool_work_entry
 One queued function call waiting for a thread-pool worker. More...
struct  thread_pool_entry
 Thread handle and shutdown metadata for one pool-owned thread. More...
struct  thread_pool
 Thread pool structure. More...

Macros

#define THREAD_POOL_NAME_BUFFER_SIZE   64

Typedefs

typedef struct thread_pool_entry thread_pool_entry_t
typedef struct thread_pool_work_entry thread_pool_work_entry_t
 One queued function call waiting for a thread-pool worker.

Functions

thread_pool_t * thread_pool_create (const char *pool_name)
 Create a new thread pool (long-lived threads mode).
thread_pool_t * thread_pool_create_with_workers (const char *pool_name, size_t num_workers)
 Create a new thread pool in work queue mode.
void thread_pool_destroy (thread_pool_t *pool)
 Destroy a thread pool.
asciichat_error_t thread_pool_queue_work (const char *name, thread_pool_t *pool, void *(*work_func)(void *), void *work_arg)
 Queue a named work item to a work queue pool.
asciichat_error_t thread_pool_spawn (thread_pool_t *pool, void *(*thread_func)(void *), void *thread_arg, int stop_id, const char *thread_name)
 Spawn a worker thread in the pool.
asciichat_error_t thread_pool_stop_all (thread_pool_t *pool)
 Stop all threads in the pool in stop_id order.
size_t thread_pool_get_count (const thread_pool_t *pool)
 Get thread count in the pool.
bool thread_pool_has_threads (const thread_pool_t *pool)
 Check if pool has any threads.
asciichat_error_t thread_pool_interrupt_all (thread_pool_t *pool, int sig)
 Send a signal to all threads in the pool.

Detailed Description

🧵 Generic thread pool abstraction for managing worker threads

Provides a reusable thread pool implementation for managing multiple worker threads with ordered cleanup support. This abstraction is used by:

  • tcp_server: Per-client thread pools for receive/send/render workers
  • Server: Background threads (stats logger, etc.)
  • ACDS: Background threads (rate limit cleanup, etc.)
  • Client: Capture and display threads

Features

  • Thread lifecycle management: Create, spawn, stop, and join threads
  • Ordered cleanup: Threads with lower stop_id values are stopped first
  • Thread naming: Assign names to threads for debugging
  • Thread-safe: All operations are protected with mutexes
  • Flexible: Supports both entity-bound pools and global singleton pools

Usage Pattern

// Create thread pool
thread_pool_t *pool = thread_pool_create("my_pool");
// Spawn workers with ordered cleanup
thread_pool_spawn(pool, worker_func1, arg1, 1, "worker1"); // Stop first
thread_pool_spawn(pool, worker_func2, arg2, 2, "worker2"); // Stop second
thread_pool_spawn(pool, worker_func3, arg3, 3, "worker3"); // Stop last
// Stop all threads in stop_id order (1 → 2 → 3)
// Destroy pool (frees all resources)
struct thread_pool thread_pool_t
One queued function call waiting for a thread-pool worker.
void thread_pool_destroy(thread_pool_t *pool)
Destroy a thread pool.
thread_pool_t * thread_pool_create(const char *pool_name)
Create a new thread pool (long-lived threads mode).
Definition thread_pool.c:92
asciichat_error_t thread_pool_spawn(thread_pool_t *pool, void *(*thread_func)(void *), void *thread_arg, int stop_id, const char *thread_name)
Spawn a worker thread in the pool.
asciichat_error_t thread_pool_stop_all(thread_pool_t *pool)
Stop all threads in the pool in stop_id order.

Ordered Cleanup Example (Server Per-Client Threads)

// Client connection established
thread_pool_spawn(pool, receive_thread, client, 1, "receive_1");
thread_pool_spawn(pool, video_render, client, 2, "video_2");
thread_pool_spawn(pool, audio_render, client, 2, "audio_2");
thread_pool_spawn(pool, send_thread, client, 3, "send_3");
// Client disconnect - threads stopped in order:
// 1. Receive thread (stop_id=1) - no new data arrives
// 2. Render threads (stop_id=2) - no new frames generated
// 3. Send thread (stop_id=3) - all queued data sent

Background Thread Example (Stats Logger)

// Server startup
thread_pool_t *server_pool = thread_pool_create("server");
thread_pool_spawn(server_pool, stats_logger, NULL, 0, "stats");
// Server shutdown
thread_pool_stop_all(server_pool);
thread_pool_destroy(server_pool);
Author
Zachary Fogg me@zf.nosp@m.o.gg
Date
December 2025

Definition in file thread_pool.h.

Macro Definition Documentation

◆ THREAD_POOL_NAME_BUFFER_SIZE

#define THREAD_POOL_NAME_BUFFER_SIZE   64

Definition at line 74 of file thread_pool.h.

Typedef Documentation

◆ thread_pool_entry_t

Definition at line 90 of file thread_pool.h.

◆ thread_pool_work_entry_t

One queued function call waiting for a thread-pool worker.

The pool stores the callback, its opaque argument, and the link to the next queued task. Queue ownership transfers to the pool when a task is submitted; workers remove and free entries after invoking the callback.

Function Documentation

◆ thread_pool_create()

thread_pool_t * thread_pool_create ( const char * pool_name)

Create a new thread pool (long-lived threads mode).

Allocates and initializes a thread pool structure. The pool starts empty with no threads. Use thread_pool_spawn() to add threads to the pool.

This mode is used for long-lived worker threads that run for the lifetime of the application or connection (e.g., stats logger, cleanup thread).

Parameters
pool_nameName for the pool (max 63 chars, used for debugging)
Returns
Pointer to new thread pool, or NULL on allocation failure

Definition at line 92 of file thread_pool.c.

92 {
94 if (!pool) {
95 SET_ERRNO(ERROR_MEMORY, "Failed to allocate thread pool");
96 return NULL;
97 }
98
99 memset(pool, 0, sizeof(*pool));
100
101 // Copy pool name (truncate if necessary)
102 if (pool_name) {
103 SAFE_STRNCPY(pool->name, pool_name, sizeof(pool->name));
104 } else {
105 SAFE_STRNCPY(pool->name, "unnamed", sizeof(pool->name));
106 }
107
108 // Initialize linked list
109 pool->threads = NULL;
110 pool->thread_count = 0;
111 pool->is_work_queue_mode = false;
112 pool->num_workers = 0;
113
114 // Initialize mutex
115 if (mutex_init(&pool->threads_mutex, "thread_pool") != 0) {
116 SAFE_FREE(pool);
117 SET_ERRNO(ERROR_THREAD, "Failed to initialize thread pool mutex");
118 return NULL;
119 }
120
121 NAMED_REGISTER_THREAD_POOL(pool, pool->name, NULL);
122
123 // Register sync primitives for debugging
124 NAMED_REGISTER_MUTEX(&pool->threads_mutex, "threads_mutex", (uintptr_t)(const void *)(pool));
125
126 log_debug("Thread pool '%s' created (long-lived thread mode)", pool->name);
127 return pool;
128}
#define SET_ERRNO(code,...)
#define SAFE_STRNCPY(dst, src, size)
Definition common.h:423
#define SAFE_FREE(ptr)
Definition common.h:377
#define SAFE_MALLOC(size, cast)
Definition common.h:265
#define NAMED_REGISTER_THREAD_POOL(pool, name, parent_ptr)
Register a thread pool with automatic format specifier.
#define NAMED_REGISTER_MUTEX(mutex, name, parent_ptr)
Register a mutex with automatic format specifier.
@ ERROR_MEMORY
Definition error_codes.h:51
@ ERROR_THREAD
#define log_debug(...)
Log a DEBUG message.
Definition log/log.h:595
mutex_t threads_mutex
Mutex protecting thread list.
bool is_work_queue_mode
True if using work queue mode.
thread_pool_entry_t * threads
Linked list of threads (sorted by stop_id).
size_t thread_count
Number of threads in pool.
size_t num_workers
Number of pre-created worker threads.
char name[64]
Pool name for debugging.

References ERROR_MEMORY, ERROR_THREAD, thread_pool::is_work_queue_mode, log_debug, thread_pool::name, NAMED_REGISTER_MUTEX, NAMED_REGISTER_THREAD_POOL, thread_pool::num_workers, SAFE_FREE, SAFE_MALLOC, SAFE_STRNCPY, SET_ERRNO, thread_pool::thread_count, thread_pool::threads, and thread_pool::threads_mutex.

Referenced by acds_server_init(), client_connection_create(), network_server_create(), and server_main().

◆ thread_pool_create_with_workers()

thread_pool_t * thread_pool_create_with_workers ( const char * pool_name,
size_t num_workers )

Create a new thread pool in work queue mode.

Creates a thread pool with pre-allocated worker threads that pull tasks from a work queue. Use thread_pool_queue_work() to add work to the pool.

This mode is used for short-lived tasks that need to be executed without creating new threads from callback context (e.g., WebSocket handlers).

Worker threads are created immediately and wait for work to be queued. The pool will not accept spawn() calls - use queue_work() instead.

Parameters
pool_nameName for the pool (max 63 chars, used for debugging)
num_workersNumber of pre-allocated worker threads
Returns
Pointer to new thread pool, or NULL on allocation failure

Definition at line 130 of file thread_pool.c.

130 {
131 if (num_workers == 0) {
132 SET_ERRNO(ERROR_INVALID_PARAM, "num_workers must be > 0");
133 return NULL;
134 }
135
137 if (!pool) {
138 SET_ERRNO(ERROR_MEMORY, "Failed to allocate thread pool");
139 return NULL;
140 }
141
142 memset(pool, 0, sizeof(*pool));
143
144 // Copy pool name
145 if (pool_name) {
146 SAFE_STRNCPY(pool->name, pool_name, sizeof(pool->name));
147 } else {
148 SAFE_STRNCPY(pool->name, "unnamed", sizeof(pool->name));
149 }
150
151 // Mark as work queue mode
152 pool->is_work_queue_mode = true;
153 pool->num_workers = num_workers;
154 pool->work_queue = NULL;
155 pool->shutdown_requested = false;
156
157 // Initialize mutexes
158 if (mutex_init(&pool->threads_mutex, "worker_list") != 0) {
159 SAFE_FREE(pool);
160 SET_ERRNO(ERROR_THREAD, "Failed to initialize threads_mutex");
161 return NULL;
162 }
163
164 if (mutex_init(&pool->work_queue_mutex, "pending_tasks") != 0) {
165 mutex_destroy(&pool->threads_mutex);
166 SAFE_FREE(pool);
167 SET_ERRNO(ERROR_THREAD, "Failed to initialize work_queue_mutex");
168 return NULL;
169 }
170
171 // Initialize condition variable
172 if (cond_init(&pool->work_available, "task_available") != 0) {
173 mutex_destroy(&pool->work_queue_mutex);
174 mutex_destroy(&pool->threads_mutex);
175 SAFE_FREE(pool);
176 SET_ERRNO(ERROR_THREAD, "Failed to initialize work_available condition");
177 return NULL;
178 }
179
180 // Create worker threads
181 for (size_t i = 0; i < num_workers; i++) {
183 if (!entry) {
184 log_error("Failed to allocate worker entry %zu", i);
186 SET_ERRNO(ERROR_MEMORY, "Failed to allocate worker entry");
187 return NULL;
188 }
189
190 memset(entry, 0, sizeof(*entry));
191 entry->stop_id = 0;
192 entry->thread_func = thread_pool_worker_thread;
193 entry->thread_arg = pool;
194 SAFE_SNPRINTF(entry->name, sizeof(entry->name), "%s_worker_%zu", pool->name, i);
195
196 // Create worker thread
197 if (platform_thread_create(&entry->thread, entry->name, thread_pool_worker_thread, pool) != 0) {
198 log_error("Failed to create worker thread %zu", i);
199 SAFE_FREE(entry);
201 SET_ERRNO(ERROR_THREAD, "Failed to create worker thread %zu", i);
202 return NULL;
203 }
204
205 // Add to thread list
206 mutex_lock(&pool->threads_mutex);
207 entry->next = pool->threads;
208 pool->threads = entry;
209 pool->thread_count++;
210 mutex_unlock(&pool->threads_mutex);
211
212 log_debug("[ThreadPool] Created worker thread %zu for pool '%s'", i, pool->name);
213 }
214
215 NAMED_REGISTER_THREAD_POOL(pool, pool->name, NULL);
216
217 // Register sync primitives for debugging
218 NAMED_REGISTER_MUTEX(&pool->threads_mutex, "threads_mutex", (uintptr_t)(const void *)(pool));
219 NAMED_REGISTER_MUTEX(&pool->work_queue_mutex, "work_queue_mutex", (uintptr_t)(const void *)(pool));
220 NAMED_REGISTER_COND(&pool->work_available, "work_available", (uintptr_t)(const void *)(pool));
221
222 log_info("[ThreadPool] Created work queue pool '%s' with %zu worker threads", pool->name, num_workers);
223 return pool;
224}
#define SAFE_SNPRINTF(buffer, buffer_size,...)
Definition common.h:507
#define NAMED_REGISTER_COND(cond, name, parent_ptr)
Register a condition variable with automatic format specifier.
@ ERROR_INVALID_PARAM
#define log_error(...)
Log an ERROR message.
Definition log/log.h:634
#define log_info(...)
Log an INFO message.
Definition log/log.h:608
platform_thread_t thread
Thread handle.
void *(* thread_func)(void *)
Thread function.
void * thread_arg
Thread argument.
struct thread_pool_entry * next
Linked list next pointer.
int stop_id
Cleanup order (lower = stop first, -1 = unordered).
char name[64]
Thread name for debugging.
cond_t work_available
Condition variable for workers to wait on.
bool shutdown_requested
Signal workers to exit.
thread_pool_work_entry_t * work_queue
Queue of pending work.
mutex_t work_queue_mutex
Mutex protecting work queue.
struct thread_pool_entry thread_pool_entry_t
Definition thread_pool.h:90

References ERROR_INVALID_PARAM, ERROR_MEMORY, ERROR_THREAD, thread_pool::is_work_queue_mode, log_debug, log_error, log_info, thread_pool::name, thread_pool_entry::name, NAMED_REGISTER_COND, NAMED_REGISTER_MUTEX, NAMED_REGISTER_THREAD_POOL, thread_pool_entry::next, thread_pool::num_workers, SAFE_FREE, SAFE_MALLOC, SAFE_SNPRINTF, SAFE_STRNCPY, SET_ERRNO, thread_pool::shutdown_requested, thread_pool_entry::stop_id, thread_pool_entry::thread, thread_pool_entry::thread_arg, thread_pool::thread_count, thread_pool_entry::thread_func, thread_pool_destroy(), thread_pool::threads, thread_pool::threads_mutex, thread_pool::work_available, thread_pool::work_queue, and thread_pool::work_queue_mutex.

Referenced by websocket_server_init().

◆ thread_pool_destroy()

void thread_pool_destroy ( thread_pool_t * pool)

Destroy a thread pool.

Stops all threads (if not already stopped), waits for them to exit, and frees all pool resources. It's safe to call this even if threads are still running - they will be stopped first.

Parameters
poolThread pool to destroy (NULL is safe, does nothing)

Definition at line 226 of file thread_pool.c.

226 {
227 if (!pool) {
228 return;
229 }
230
231 log_debug("Destroying thread pool '%s' (thread_count=%zu, work_queue_mode=%d)", pool->name, pool->thread_count,
232 pool->is_work_queue_mode);
233
234 // Stop all threads first (if not already stopped)
235 if (pool->thread_count > 0) {
236 log_debug("Thread pool '%s' has %zu threads, stopping them first", pool->name, pool->thread_count);
238 log_debug("After thread_pool_stop_all: pool->threads=%p, pool->thread_count=%zu", (void *)pool->threads,
239 pool->thread_count);
240 }
241
242 mutex_lock(&pool->threads_mutex);
243 bool threads_still_running = pool->thread_count > 0;
244 mutex_unlock(&pool->threads_mutex);
245 if (threads_still_running) {
246 // A timed-out thread still owns access to this pool. Retain the pool and
247 // its synchronization objects so that worker cannot race freed memory.
248 log_error("Keeping thread pool '%s' alive because %zu worker thread(s) have not exited", pool->name,
249 pool->thread_count);
250 return;
251 }
252
253 // Clean up work queue (in case there's pending work)
254 if (pool->is_work_queue_mode) {
255 mutex_lock(&pool->work_queue_mutex);
257 while (entry) {
258 thread_pool_work_entry_t *next = entry->next;
259 SAFE_FREE(entry);
260 entry = next;
261 }
262 pool->work_queue = NULL;
263 mutex_unlock(&pool->work_queue_mutex);
264
265 // Destroy condition variable and work queue mutex
266 cond_destroy(&pool->work_available);
267 mutex_destroy(&pool->work_queue_mutex);
268 }
269
270 // Destroy threads mutex
271 mutex_destroy(&pool->threads_mutex);
272
273 NAMED_UNREGISTER(pool);
274
275 // Free pool
276 log_debug("About to free thread pool '%s' at %p", pool->name, (void *)pool);
277 SAFE_FREE(pool);
278
279 log_debug("Thread pool destroyed (freed memory)");
280}
#define NAMED_UNREGISTER(ptr)
Unregister a pointer.
struct thread_pool_work_entry * next
Linked list next pointer.
struct thread_pool_work_entry thread_pool_work_entry_t
One queued function call waiting for a thread-pool worker.

References thread_pool::is_work_queue_mode, log_debug, log_error, thread_pool::name, NAMED_UNREGISTER, thread_pool_work_entry::next, SAFE_FREE, thread_pool::thread_count, thread_pool_stop_all(), thread_pool::threads, thread_pool::threads_mutex, thread_pool::work_available, thread_pool::work_queue, and thread_pool::work_queue_mutex.

Referenced by acds_server_shutdown(), client_connection_destroy(), network_server_destroy(), thread_pool_create_with_workers(), and websocket_server_destroy().

◆ thread_pool_get_count()

size_t thread_pool_get_count ( const thread_pool_t * pool)

Get thread count in the pool.

Thread-safe count of active threads in the pool.

Parameters
poolThread pool to query
Returns
Number of threads in pool, or 0 if pool is NULL

Definition at line 493 of file thread_pool.c.

493 {
494 if (!pool) {
495 return 0;
496 }
497
498 // Note: We're not locking here for performance
499 // thread_count is a simple size_t which should be atomic on most platforms
500 // If strict thread safety is required, use mutex_lock/unlock
501 return pool->thread_count;
502}

References thread_pool::thread_count.

Referenced by thread_pool_has_threads().

◆ thread_pool_has_threads()

bool thread_pool_has_threads ( const thread_pool_t * pool)

Check if pool has any threads.

Convenience function equivalent to thread_pool_get_count(pool) > 0.

Parameters
poolThread pool to check
Returns
true if pool has threads, false if empty or NULL

Definition at line 504 of file thread_pool.c.

504 {
505 return thread_pool_get_count(pool) > 0;
506}
size_t thread_pool_get_count(const thread_pool_t *pool)
Get thread count in the pool.

References thread_pool_get_count().

◆ thread_pool_interrupt_all()

asciichat_error_t thread_pool_interrupt_all ( thread_pool_t * pool,
int sig )

Send a signal to all threads in the pool.

Sends the specified signal to every thread in the pool. This is used to interrupt threads that are blocked in system calls (e.g., recv(), connect()) so they unblock and can check the global exit flag.

Thread-safe: acquires threads_mutex to iterate the thread list. Not async-signal-safe: cannot be called from signal handlers.

On POSIX systems, uses pthread_kill() to send the signal. On Windows, this is a no-op (pthread_kill is not available, but socket_shutdown() is sufficient for Windows socket unblocking).

Typically used with SIGUSR1 (which should have a registered no-op handler) or another signal that the threads are prepared to handle gracefully.

Parameters
poolThread pool to interrupt
sigSignal number to send (e.g., SIGUSR1)
Returns
ASCIICHAT_OK on success, error code on failure

Definition at line 508 of file thread_pool.c.

508 {
509 if (!pool) {
510 return SET_ERRNO(ERROR_INVALID_PARAM, "pool is NULL");
511 }
512
513#ifndef _WIN32
514 // Only available on POSIX systems (Unix/Linux/macOS)
515 mutex_lock(&pool->threads_mutex);
516
517 if (pool->thread_count == 0) {
518 mutex_unlock(&pool->threads_mutex);
519 log_debug("Thread pool '%s' has no threads to interrupt", pool->name);
520 return ASCIICHAT_OK;
521 }
522
523 log_debug("Sending signal %d to %zu threads in pool '%s'", sig, pool->thread_count, pool->name);
524
525 // Iterate through all threads and send signal
526 thread_pool_entry_t *entry = pool->threads;
527 int sent_count = 0;
528 int failed_count = 0;
529
530 while (entry) {
531 // pthread_kill returns 0 on success, non-zero error code on failure
532 if (pthread_kill(entry->thread, sig) != 0) {
533 log_warn("Failed to send signal %d to thread '%s' in pool '%s'", sig, entry->name, pool->name);
534 failed_count++;
535 } else {
536 sent_count++;
537 }
538 entry = entry->next;
539 }
540
541 mutex_unlock(&pool->threads_mutex);
542
543 log_debug("Sent signal %d to %d/%zu threads in pool '%s' (%d failures)", sig, sent_count, pool->thread_count,
544 pool->name, failed_count);
545
546 if (failed_count > 0 && sent_count == 0) {
547 return SET_ERRNO(ERROR_THREAD, "Failed to send signal to any threads in pool '%s'", pool->name);
548 }
549
550 return ASCIICHAT_OK;
551#else
552 // Windows: no pthread_kill equivalent needed - socket_shutdown() is sufficient
553 (void)sig; // Unused on Windows
554 log_debug("no-op on Windows (socket shutdown is sufficient)");
555 return ASCIICHAT_OK;
556#endif
557}
@ ASCIICHAT_OK
Definition error_codes.h:46
#define log_warn(...)
Log a WARN message.
Definition log/log.h:621

References ASCIICHAT_OK, ERROR_INVALID_PARAM, ERROR_THREAD, log_debug, log_warn, thread_pool::name, thread_pool_entry::name, thread_pool_entry::next, SET_ERRNO, thread_pool_entry::thread, thread_pool::thread_count, thread_pool::threads, and thread_pool::threads_mutex.

◆ thread_pool_queue_work()

asciichat_error_t thread_pool_queue_work ( const char * name,
thread_pool_t * pool,
void *(* work_func )(void *),
void * work_arg )

Queue a named work item to a work queue pool.

Adds a named work item to the pool's work queue. One of the pre-created worker threads will pick up the work and execute it. This function is only valid for pools created with thread_pool_create_with_workers(). The work is automatically registered with the debug naming system.

Safe to call from callback context (e.g., LWS callbacks). Work is queued without creating new threads.

Parameters
nameDebug name for the work item
poolThread pool to queue work to (must be in work queue mode)
work_funcFunction to execute
work_argArgument passed to work function
Returns
ASCIICHAT_OK on success, error code on failure

Definition at line 282 of file thread_pool.c.

283 {
284 if (!name) {
285 return SET_ERRNO(ERROR_INVALID_PARAM, "work name is required");
286 }
287
288 if (!pool) {
289 return SET_ERRNO(ERROR_INVALID_PARAM, "pool is NULL");
290 }
291
292 if (!pool->is_work_queue_mode) {
293 return SET_ERRNO(ERROR_INVALID_STATE, "Pool not in work queue mode. Use thread_pool_create_with_workers()");
294 }
295
296 if (!work_func) {
297 return SET_ERRNO(ERROR_INVALID_PARAM, "work_func is NULL");
298 }
299
300 // Allocate work entry
302 if (!entry) {
303 return SET_ERRNO(ERROR_MEMORY, "Failed to allocate work entry");
304 }
305
306 entry->work_func = work_func;
307 entry->work_arg = work_arg;
308 entry->next = NULL;
309
310 // Register work with debug naming system
311 NAMED_REGISTER_THREADPOOL_WORK(entry, name, NULL);
312
313 // Add to work queue (at the end)
314 mutex_lock(&pool->work_queue_mutex);
315
316 // Find the last entry in the queue
317 if (!pool->work_queue) {
318 pool->work_queue = entry;
319 } else {
321 while (last->next) {
322 last = last->next;
323 }
324 last->next = entry;
325 }
326
327 // Signal workers that work is available (WHILE HOLDING MUTEX to prevent lost wakeup race)
328 cond_signal(&pool->work_available);
329
330 mutex_unlock(&pool->work_queue_mutex);
331
332 log_dev("Queued work to pool '%s' (workers=%zu)", pool->name, pool->num_workers);
333 return ASCIICHAT_OK;
334}
#define NAMED_REGISTER_THREADPOOL_WORK(work, name, parent_ptr)
Register a thread pool work item with automatic format specifier.
@ ERROR_INVALID_STATE
#define log_dev(...)
Log a DEV message (most verbose, development only).
Definition log/log.h:581
void * work_arg
Argument passed to work function.
void *(* work_func)(void *)
Work function to execute.

References ASCIICHAT_OK, ERROR_INVALID_PARAM, ERROR_INVALID_STATE, ERROR_MEMORY, thread_pool::is_work_queue_mode, log_dev, thread_pool::name, NAMED_REGISTER_THREADPOOL_WORK, thread_pool_work_entry::next, thread_pool::num_workers, SAFE_MALLOC, SET_ERRNO, thread_pool_work_entry::work_arg, thread_pool::work_available, thread_pool_work_entry::work_func, thread_pool::work_queue, and thread_pool::work_queue_mutex.

◆ thread_pool_spawn()

asciichat_error_t thread_pool_spawn ( thread_pool_t * pool,
void *(* thread_func )(void *),
void * thread_arg,
int stop_id,
const char * thread_name )

Spawn a worker thread in the pool.

Creates and tracks a new worker thread with optional ordered cleanup. Threads are inserted into the pool's linked list in sorted order by stop_id (ascending). When the pool is stopped, threads with lower stop_id values are stopped first.

Stop ID Ordering Guidelines:

  • stop_id < 0: Unordered (stopped last, order undefined)
  • stop_id = 0: General background workers
  • stop_id = 1: Data source threads (e.g., receive, capture)
  • stop_id = 2: Processing threads (e.g., render, encode)
  • stop_id = 3: Data sink threads (e.g., send, write)

Example (Server Per-Client Threads):

  • stop_id=1: Receive thread (stop first to prevent new data)
  • stop_id=2: Render threads (stop after receive)
  • stop_id=3: Send thread (stop last after all processing)
Parameters
poolThread pool to spawn into
thread_funcThread function to execute
thread_argArgument passed to thread function
stop_idCleanup order (-1=unordered, 0+=ordered)
thread_nameThread name for debugging (max 63 chars, NULL=auto-generate)
Returns
ASCIICHAT_OK on success, error code on failure

Definition at line 336 of file thread_pool.c.

337 {
338 if (!pool) {
339 return SET_ERRNO(ERROR_INVALID_PARAM, "pool is NULL");
340 }
341
342 if (pool->is_work_queue_mode) {
343 return SET_ERRNO(ERROR_INVALID_STATE, "Cannot spawn threads in work queue mode. Use thread_pool_queue_work()");
344 }
345
346 if (!thread_func) {
347 return SET_ERRNO(ERROR_INVALID_PARAM, "thread_func is NULL");
348 }
349
350 // Allocate thread entry
352 if (!entry) {
353 return SET_ERRNO(ERROR_MEMORY, "Failed to allocate thread pool entry");
354 }
355
356 memset(entry, 0, sizeof(*entry));
357 entry->stop_id = stop_id;
358 entry->thread_func = thread_func;
359 entry->thread_arg = thread_arg;
360 entry->next = NULL;
361
362 // Copy thread name (truncate if necessary)
363 if (thread_name) {
364 SAFE_STRNCPY(entry->name, thread_name, sizeof(entry->name));
365 } else {
366 // Auto-generate name
367 SAFE_SNPRINTF(entry->name, sizeof(entry->name), "%s_worker_%d", pool->name, stop_id);
368 }
369
370 // Create thread
371 if (platform_thread_create(&entry->thread, entry->name, thread_func, thread_arg) != 0) {
372 // Save name before freeing entry
373 char thread_name_copy[THREAD_POOL_THREAD_NAME_COPY_BUFFER_SIZE];
374 SAFE_STRNCPY(thread_name_copy, entry->name, sizeof(thread_name_copy));
375 SAFE_FREE(entry);
376 return SET_ERRNO(ERROR_THREAD, "Failed to create thread '%s' in pool '%s'", thread_name_copy, pool->name);
377 }
378
379 // Add to thread list (sorted by stop_id)
380 mutex_lock(&pool->threads_mutex);
381
382 if (!pool->threads || pool->threads->stop_id > stop_id) {
383 // Insert at head
384 entry->next = pool->threads;
385 pool->threads = entry;
386 } else {
387 // Find insertion point (maintain sorted order by stop_id)
388 thread_pool_entry_t *prev = pool->threads;
389 while (prev->next && prev->next->stop_id <= stop_id) {
390 prev = prev->next;
391 }
392 entry->next = prev->next;
393 prev->next = entry;
394 }
395
396 pool->thread_count++;
397 mutex_unlock(&pool->threads_mutex);
398
399 log_debug("Spawned thread '%s' (stop_id=%d) in pool '%s' (total_threads=%zu)", entry->name, stop_id, pool->name,
400 pool->thread_count);
401
402 return ASCIICHAT_OK;
403}
#define THREAD_POOL_THREAD_NAME_COPY_BUFFER_SIZE
Definition thread_pool.c:7

References ASCIICHAT_OK, ERROR_INVALID_PARAM, ERROR_INVALID_STATE, ERROR_MEMORY, ERROR_THREAD, thread_pool::is_work_queue_mode, log_debug, thread_pool::name, thread_pool_entry::name, thread_pool_entry::next, SAFE_FREE, SAFE_MALLOC, SAFE_SNPRINTF, SAFE_STRNCPY, SET_ERRNO, thread_pool_entry::stop_id, thread_pool_entry::thread, thread_pool_entry::thread_arg, thread_pool::thread_count, thread_pool_entry::thread_func, THREAD_POOL_THREAD_NAME_COPY_BUFFER_SIZE, thread_pool::threads, and thread_pool::threads_mutex.

Referenced by acds_server_init(), audio_sender_init(), audio_start_thread(), capture_start_thread(), client_protocol_start(), keepalive_start_thread(), and server_main().

◆ thread_pool_stop_all()

asciichat_error_t thread_pool_stop_all ( thread_pool_t * pool)

Stop all threads in the pool in stop_id order.

Joins all threads in ascending stop_id order (lower values first). Threads with negative stop_id are stopped last in undefined order. This function blocks until all threads have exited.

Shutdown Sequence:

  1. Threads with stop_id >= 0 are joined in ascending order
  2. Threads with stop_id < 0 are joined in any order
  3. All thread entries are freed

After this call, the pool is empty but still valid - you can spawn new threads or destroy the pool.

Parameters
poolThread pool to stop
Returns
ASCIICHAT_OK on success, error code on failure

Definition at line 405 of file thread_pool.c.

405 {
406 if (!pool) {
407 return SET_ERRNO(ERROR_INVALID_PARAM, "pool is NULL");
408 }
409
410 mutex_lock(&pool->threads_mutex);
411
412 if (pool->thread_count == 0) {
413 mutex_unlock(&pool->threads_mutex);
414 log_debug("Thread pool '%s' has no threads to stop", pool->name);
415 return ASCIICHAT_OK;
416 }
417
418 log_debug("Stopping %zu threads in pool '%s' in stop_id order", pool->thread_count, pool->name);
419
420 // For work queue mode, signal workers to shutdown (BEFORE releasing the mutex to prevent new work)
421 if (pool->is_work_queue_mode) {
422 mutex_lock(&pool->work_queue_mutex);
423 pool->shutdown_requested = true;
424 cond_broadcast(&pool->work_available);
425 mutex_unlock(&pool->work_queue_mutex);
426 }
427
428 // Detach the current list so workers can finish without holding the pool
429 // mutex. Any thread that misses its join deadline is reinserted below.
430 thread_pool_entry_t *threads_to_join = pool->threads;
431 pool->threads = NULL;
432 pool->thread_count = 0;
433
434 // Release mutex BEFORE joining threads to prevent deadlock if workers need to acquire it
435 mutex_unlock(&pool->threads_mutex);
436
437 // Now join threads (WITHOUT holding the mutex to prevent deadlock)
438 thread_pool_entry_t *entry = threads_to_join;
439 int joined_count = 0, freed_count = 0, still_running_count = 0;
440 thread_pool_entry_t *still_running = NULL;
441 const uint64_t THREAD_JOIN_TIMEOUT_NS = 2 * 1000000000ULL; // 2 second timeout per thread
442
443 while (entry) {
444 log_debug("Joining thread '%s' (stop_id=%d) in pool '%s' with 2s timeout", entry->name, entry->stop_id, pool->name);
445 thread_pool_entry_t *next = entry->next;
446
447 // A timed-out worker may still access its pool and arguments. Keep its
448 // entry tracked so later shutdown can join it before freeing those objects.
449 int join_result = thread_join_timeout(&entry->thread, NULL, THREAD_JOIN_TIMEOUT_NS);
450 if (join_result != 0) {
451 if (join_result == ETIMEDOUT) {
452 log_warn("Thread '%s' (stop_id=%d) did not exit within 2s timeout; retaining its pool entry.", entry->name,
453 entry->stop_id);
454 } else {
455 log_error("Failed to join thread '%s' in pool '%s' (error %d); retaining its pool entry", entry->name,
456 pool->name, join_result);
457 }
458 entry->next = still_running;
459 still_running = entry;
460 still_running_count++;
461 entry = next;
462 continue;
463 } else {
464 joined_count++;
465 }
466
467 log_debug("Freeing thread_pool_entry '%s' at %p", entry->name, (void *)entry);
468 SAFE_FREE(entry);
469 freed_count++;
470 entry = next;
471 }
472
473 if (still_running) {
474 mutex_lock(&pool->threads_mutex);
475 while (still_running) {
476 thread_pool_entry_t *entry = still_running;
477 still_running = still_running->next;
478 entry->next = NULL;
479 thread_pool_insert_entry_locked(pool, entry);
480 }
481 mutex_unlock(&pool->threads_mutex);
482 }
483
484 log_debug("joined=%d, freed=%d", joined_count, freed_count);
485
486 log_debug("All threads stopped in pool '%s'", pool->name);
487 if (still_running_count > 0) {
488 return SET_ERRNO(ERROR_THREAD, "%d thread(s) in pool '%s' did not stop", still_running_count, pool->name);
489 }
490 return ASCIICHAT_OK;
491}
unsigned long long uint64_t
Definition common.h:63
#define ETIMEDOUT

References ASCIICHAT_OK, ERROR_INVALID_PARAM, ERROR_THREAD, ETIMEDOUT, thread_pool::is_work_queue_mode, log_debug, log_error, log_warn, thread_pool::name, thread_pool_entry::name, thread_pool_entry::next, SAFE_FREE, SET_ERRNO, thread_pool::shutdown_requested, thread_pool_entry::stop_id, thread_pool_entry::thread, thread_pool::thread_count, thread_pool::threads, thread_pool::threads_mutex, thread_pool::work_available, and thread_pool::work_queue_mutex.

Referenced by network_server_stop(), and thread_pool_destroy().