diff --git a/Makefile b/Makefile
index 9995f85..3c89d56 100644
--- a/Makefile
+++ b/Makefile
@@ -13,12 +13,13 @@ all: libpool-example.out
benchmark: benchmark.out
./benchmark.sh
-test: libpool-test.out
+test: libpool-test.out libpool-test-mt.out
./libpool-test.out
+ ./libpool-test-mt.out
clean:
rm -rf obj/*
- rm -f libpool-example.out libpool-test.out benchmark.out
+ rm -f libpool-example.out libpool-test.out libpool-test-mt.out benchmark.out
#-------------------------------------------------------------------------------
@@ -29,3 +30,6 @@ obj/%.c.o: %.c
%.out: obj/examples/%.c.o obj/src/libpool.c.o
@mkdir -p $(dir $@)
$(CC) $(CFLAGS) $(CPPFLAGS) -o $@ $^ $(LDLIBS)
+
+libpool-test-mt.out: examples/libpool-test-mt.c src/libpool.c
+ $(CC) $(CFLAGS) $(CPPFLAGS) -DLIBPOOL_THREAD_SAFE -o $@ $^ -lpthread
diff --git a/examples/libpool-test-mt.c b/examples/libpool-test-mt.c
new file mode 100644
index 0000000..8d79537
--- /dev/null
+++ b/examples/libpool-test-mt.c
@@ -0,0 +1,250 @@
+/*
+ * Copyright 2024 8dcc
+ *
+ * This program is part of libpool, a tiny (ANSI) C library for pool allocation.
+ *
+ * This program is free software: you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License as published by the Free Software
+ * Foundation, either version 3 of the License, or any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+ * FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
+ * details.
+ *
+ * You should have received a copy of the GNU General Public License along with
+ * this program. If not, see .
+ *
+ * Multithreading tests for libpool. Must be compiled with
+ * -DLIBPOOL_THREAD_SAFE.
+ */
+
+#include
+#include
+#include
+#include
+
+#include "../src/libpool.h"
+#include "test.h"
+
+#if !defined(LIBPOOL_THREAD_SAFE)
+#error "This test must be compiled with -DLIBPOOL_THREAD_SAFE"
+#endif /* !defined(LIBPOOL_THREAD_SAFE) */
+
+/*----------------------------------------------------------------------------*/
+/* Shared state */
+
+#define NUM_THREADS 4
+#define ALLOCS_PER_THR 50
+
+static Pool* shared_pool = NULL;
+static unsigned long successful_allocs = 0;
+static pthread_mutex_t counter_lock;
+
+/*----------------------------------------------------------------------------*/
+/* Test: basic alloc/free from multiple threads */
+
+static void* basic_thread_func(void* arg) {
+ void* chunks[ALLOCS_PER_THR];
+ int allocated = 0;
+ size_t i;
+
+ (void)arg; /* Unused */
+
+ for (i = 0; i < ALLOCS_PER_THR; i++) {
+ chunks[i] = pool_alloc(shared_pool);
+ if (chunks[i] != NULL)
+ allocated++;
+ }
+
+ for (i = 0; i < ALLOCS_PER_THR; i++)
+ if (chunks[i] != NULL)
+ pool_free(shared_pool, chunks[i]);
+
+ pthread_mutex_lock(&counter_lock);
+ successful_allocs += allocated;
+ pthread_mutex_unlock(&counter_lock);
+
+ return NULL;
+}
+
+TEST_DECL(basic_alloc_free) {
+ pthread_t threads[NUM_THREADS];
+ size_t i;
+
+ shared_pool = pool_new(NUM_THREADS * ALLOCS_PER_THR, sizeof(long));
+ successful_allocs = 0;
+ TEST_ASSERT_NOT_NULL(shared_pool);
+
+ pthread_mutex_init(&counter_lock, NULL);
+
+ for (i = 0; i < NUM_THREADS; i++)
+ pthread_create(&threads[i], NULL, basic_thread_func, NULL);
+
+ for (i = 0; i < NUM_THREADS; i++)
+ pthread_join(threads[i], NULL);
+
+ pthread_mutex_destroy(&counter_lock);
+
+ TEST_ASSERT_EQ(successful_allocs, NUM_THREADS * ALLOCS_PER_THR);
+
+ pool_destroy(shared_pool);
+}
+
+/*----------------------------------------------------------------------------*/
+/* Test: threads competing for limited pool capacity */
+
+static void* contention_thread_func(void* arg) {
+ void* chunks[ALLOCS_PER_THR];
+ int allocated = 0;
+ size_t i;
+
+ (void)arg; /* Unused */
+
+ for (i = 0; i < ALLOCS_PER_THR; i++) {
+ chunks[i] = pool_alloc(shared_pool);
+ if (chunks[i] != NULL)
+ allocated++;
+ }
+
+ for (i = 0; i < ALLOCS_PER_THR; i++)
+ if (chunks[i] != NULL)
+ pool_free(shared_pool, chunks[i]);
+
+ pthread_mutex_lock(&counter_lock);
+ successful_allocs += allocated;
+ pthread_mutex_unlock(&counter_lock);
+
+ return NULL;
+}
+
+TEST_DECL(contention) {
+ const size_t pool_size = 25;
+ pthread_t threads[NUM_THREADS];
+ size_t i;
+
+ /* Pool smaller than total demand: threads must compete */
+ shared_pool = pool_new(pool_size, sizeof(long));
+ successful_allocs = 0;
+ TEST_ASSERT_NOT_NULL(shared_pool);
+
+ pthread_mutex_init(&counter_lock, NULL);
+
+ for (i = 0; i < NUM_THREADS; i++)
+ pthread_create(&threads[i], NULL, contention_thread_func, NULL);
+
+ for (i = 0; i < NUM_THREADS; i++)
+ pthread_join(threads[i], NULL);
+
+ pthread_mutex_destroy(&counter_lock);
+
+ /* Total successful allocations should not exceed pool capacity */
+ TEST_ASSERT_TRUE(successful_allocs >= pool_size);
+
+ pool_destroy(shared_pool);
+}
+
+/*----------------------------------------------------------------------------*/
+/* Test: rapid alloc/free cycles to stress locking */
+
+static void* rapid_cycle_thread_func(void* arg) {
+ void* chunk;
+ size_t i;
+
+ (void)arg; /* Unused */
+
+ /* Rapidly allocate and free 100 times from each thread */
+ for (i = 0; i < 100; i++) {
+ chunk = pool_alloc(shared_pool);
+ if (chunk != NULL)
+ pool_free(shared_pool, chunk);
+ }
+
+ return NULL;
+}
+
+TEST_DECL(rapid_cycles) {
+ pthread_t threads[NUM_THREADS];
+ size_t i;
+
+ shared_pool = pool_new(NUM_THREADS, sizeof(long));
+ TEST_ASSERT_NOT_NULL(shared_pool);
+
+ for (i = 0; i < NUM_THREADS; i++)
+ pthread_create(&threads[i], NULL, rapid_cycle_thread_func, NULL);
+
+ for (i = 0; i < NUM_THREADS; i++)
+ pthread_join(threads[i], NULL);
+
+ pool_destroy(shared_pool);
+}
+
+/*----------------------------------------------------------------------------*/
+/* Test: concurrent expand while allocating/freeing */
+
+static void* expand_alloc_thread_func(void* arg) {
+ void* chunks[ALLOCS_PER_THR];
+ size_t i, round;
+
+ (void)arg; /* Unused */
+
+ for (round = 0; round < 10; round++) {
+ for (i = 0; i < ALLOCS_PER_THR; i++)
+ chunks[i] = pool_alloc(shared_pool);
+
+ for (i = 0; i < ALLOCS_PER_THR; i++)
+ if (chunks[i] != NULL)
+ pool_free(shared_pool, chunks[i]);
+ }
+
+ return NULL;
+}
+
+static void* expand_thread_func(void* arg) {
+ size_t i;
+
+ (void)arg; /* Unused */
+
+ for (i = 0; i < 10; i++)
+ pool_expand(shared_pool, 10);
+
+ return NULL;
+}
+
+TEST_DECL(concurrent_expand) {
+ pthread_t alloc_threads[NUM_THREADS];
+ pthread_t expand_thread;
+ size_t i;
+
+ shared_pool = pool_new(NUM_THREADS * ALLOCS_PER_THR, sizeof(long));
+ TEST_ASSERT_NOT_NULL(shared_pool);
+
+ /* Create N allocation threads, and one expansion thread */
+ for (i = 0; i < NUM_THREADS; i++)
+ pthread_create(&alloc_threads[i], NULL, expand_alloc_thread_func, NULL);
+ pthread_create(&expand_thread, NULL, expand_thread_func, NULL);
+
+ /* Wait for the threads to finish */
+ for (i = 0; i < NUM_THREADS; i++)
+ pthread_join(alloc_threads[i], NULL);
+ pthread_join(expand_thread, NULL);
+
+ pool_destroy(shared_pool);
+}
+
+/*----------------------------------------------------------------------------*/
+/* Main */
+
+int main(void) {
+ printf("Running libpool multithreading tests...\n\n");
+
+ TEST_RUN(basic_alloc_free);
+ TEST_RUN(contention);
+ TEST_RUN(rapid_cycles);
+ TEST_RUN(concurrent_expand);
+
+ putchar('\n');
+ TEST_PRINT_RESULTS(stdout);
+
+ return (TEST_NUM_FAILED > 0) ? 1 : 0;
+}
diff --git a/src/libpool.c b/src/libpool.c
index baf2282..0a7efb8 100644
--- a/src/libpool.c
+++ b/src/libpool.c
@@ -34,16 +34,46 @@ PoolAllocFuncPtr pool_ext_alloc = malloc;
PoolFreeFuncPtr pool_ext_free = free;
#endif /* !defined(LIBPOOL_NO_STDLIB) */
+/*
+ * External multithreading functions.
+ */
+#if defined(LIBPOOL_THREAD_SAFE)
+#if defined(LIBPOOL_NO_STDLIB)
+PoolMutexInitFuncPtr pool_ext_mutex_init = NULL;
+PoolMutexLockFuncPtr pool_ext_mutex_lock = NULL;
+PoolMutexUnlockFuncPtr pool_ext_mutex_unlock = NULL;
+PoolMutexDestroyFuncPtr pool_ext_mutex_destroy = NULL;
+#else /* !defined(LIBPOOL_NO_STDLIB) */
+#include
+static bool pool_ext_mutex_init_impl(pool_ext_mutex_t* mutex) {
+ return pthread_mutex_init(mutex, NULL) == 0;
+}
+static bool pool_ext_mutex_lock_impl(pool_ext_mutex_t* mutex) {
+ return pthread_mutex_lock(mutex) == 0;
+}
+static bool pool_ext_mutex_unlock_impl(pool_ext_mutex_t* mutex) {
+ return pthread_mutex_unlock(mutex) == 0;
+}
+static bool pool_ext_mutex_destroy_impl(pool_ext_mutex_t* mutex) {
+ return pthread_mutex_destroy(mutex) == 0;
+}
+PoolMutexInitFuncPtr pool_ext_mutex_init = pool_ext_mutex_init_impl;
+PoolMutexLockFuncPtr pool_ext_mutex_lock = pool_ext_mutex_lock_impl;
+PoolMutexUnlockFuncPtr pool_ext_mutex_unlock = pool_ext_mutex_unlock_impl;
+PoolMutexDestroyFuncPtr pool_ext_mutex_destroy = pool_ext_mutex_destroy_impl;
+#endif /* !defined(LIBPOOL_NO_STDLIB) */
+#endif /* defined(LIBPOOL_THEAD_SAFE) */
+
/*
* External valgrind macros.
*/
#if defined(LIBPOOL_NO_VALGRIND)
-#define VALGRIND_CREATE_MEMPOOL(a, b, c) ((void)0)
-#define VALGRIND_DESTROY_MEMPOOL(a) ((void)0)
-#define VALGRIND_MEMPOOL_ALLOC(a, b, c) ((void)0)
-#define VALGRIND_MEMPOOL_FREE(a, b) ((void)0)
-#define VALGRIND_MAKE_MEM_DEFINED(a, b) ((void)0)
-#define VALGRIND_MAKE_MEM_NOACCESS(a, b) ((void)0)
+#define VALGRIND_CREATE_MEMPOOL(A, B, C) ((void)0)
+#define VALGRIND_DESTROY_MEMPOOL(A) ((void)0)
+#define VALGRIND_MEMPOOL_ALLOC(A, B, C) ((void)0)
+#define VALGRIND_MEMPOOL_FREE(A, B) ((void)0)
+#define VALGRIND_MAKE_MEM_DEFINED(A, B) ((void)0)
+#define VALGRIND_MAKE_MEM_NOACCESS(A, B) ((void)0)
#else /* !defined(LIBPOOL_NO_VALGRIND) */
#include
#include
@@ -60,7 +90,7 @@ PoolFreeFuncPtr pool_ext_free = free;
#define ALIGN2BOUNDARY(ADDR, BOUND) (ADDR)
#else /* !defined(LIBPOOL_NO_ALIGNMENT) */
#define ALIGN2BOUNDARY(SIZE, BOUNDARY) \
- (((SIZE) + (BOUNDARY) - 1) & ~((BOUNDARY) - 1))
+ (((SIZE) + (BOUNDARY)-1) & ~((BOUNDARY)-1))
#endif /* !defined(LIBPOOL_NO_ALIGNMENT) */
#if !defined(LIBPOOL_LOG)
@@ -97,6 +127,10 @@ struct Pool {
void* free_chunk;
ArrayStart* array_starts;
size_t chunk_sz;
+
+#if defined(LIBPOOL_THREAD_SAFE)
+ pool_ext_mutex_t lock;
+#endif /* defined(LIBPOOL_THREAD_SAFE) */
};
/*----------------------------------------------------------------------------*/
@@ -160,6 +194,16 @@ Pool* pool_new(size_t pool_sz, size_t chunk_sz) {
return NULL;
}
+#if defined(LIBPOOL_THREAD_SAFE)
+ if (!pool_ext_mutex_init(&pool->lock)) {
+ LIBPOOL_LOG("Failed to initialize mutex.");
+ pool_ext_free(arr);
+ pool_ext_free(pool->array_starts);
+ pool_ext_free(pool);
+ return NULL;
+ }
+#endif /* defined(LIBPOOL_THREAD_SAFE) */
+
/*
* Build the linked list. Use the first N bytes of the free chunks for
* storing the (hypothetical) `.next' pointer. This is why `chunk_sz' must
@@ -193,6 +237,7 @@ Pool* pool_new(size_t pool_sz, size_t chunk_sz) {
* 5. Prepend the new `ArrayStart' to the existing linked list of array starts.
*/
bool pool_expand(Pool* pool, size_t extra_sz) {
+ bool result = true;
ArrayStart* array_start;
char* extra_arr;
size_t i;
@@ -202,19 +247,28 @@ bool pool_expand(Pool* pool, size_t extra_sz) {
return false;
}
+#if defined(LIBPOOL_THREAD_SAFE)
+ if (!pool_ext_mutex_lock(&pool->lock)) {
+ LIBPOOL_LOG("Failed to lock mutex.");
+ return false;
+ }
+#endif /* defined(LIBPOOL_THREAD_SAFE) */
+
VALGRIND_MAKE_MEM_DEFINED(pool, sizeof(Pool));
array_start = pool_ext_alloc(sizeof(ArrayStart));
if (array_start == NULL) {
LIBPOOL_LOG("Failed to allocate additional 'ArrayStart' structure.");
- return false;
+ result = false;
+ goto alloc_err;
}
extra_arr = pool_ext_alloc(extra_sz * pool->chunk_sz);
if (extra_arr == NULL) {
LIBPOOL_LOG("Failed to allocate additional data array.");
pool_ext_free(array_start);
- return false;
+ result = false;
+ goto alloc_err;
}
for (i = 0; i < extra_sz - 1; i++)
@@ -230,9 +284,14 @@ bool pool_expand(Pool* pool, size_t extra_sz) {
VALGRIND_MAKE_MEM_NOACCESS(extra_arr, extra_sz * pool->chunk_sz);
VALGRIND_MAKE_MEM_NOACCESS(array_start, sizeof(ArrayStart));
+alloc_err:
VALGRIND_MAKE_MEM_NOACCESS(pool, sizeof(Pool));
- return true;
+#if defined(LIBPOOL_THREAD_SAFE)
+ pool_ext_mutex_unlock(&pool->lock);
+#endif /* defined(LIBPOOL_THREAD_SAFE) */
+
+ return result;
}
/*
@@ -249,6 +308,13 @@ void pool_destroy(Pool* pool) {
return;
}
+#if defined(LIBPOOL_THREAD_SAFE)
+ if (!pool_ext_mutex_lock(&pool->lock)) {
+ LIBPOOL_LOG("Failed to lock mutex.");
+ return;
+ }
+#endif /* defined(LIBPOOL_THREAD_SAFE) */
+
VALGRIND_MAKE_MEM_DEFINED(pool, sizeof(Pool));
array_start = pool->array_starts;
@@ -261,6 +327,11 @@ void pool_destroy(Pool* pool) {
array_start = next;
}
+#if defined(LIBPOOL_THREAD_SAFE)
+ pool_ext_mutex_unlock(&pool->lock);
+ pool_ext_mutex_destroy(&pool->lock);
+#endif /* defined(LIBPOOL_THREAD_SAFE) */
+
VALGRIND_DESTROY_MEMPOOL(pool);
pool_ext_free(pool);
}
@@ -274,17 +345,25 @@ void pool_destroy(Pool* pool) {
* linked list to the second item of the old list.
*/
void* pool_alloc(Pool* pool) {
- void* result;
+ void* result = NULL;
if (pool == NULL) {
LIBPOOL_LOG("Invalid pool pointer.");
return NULL;
}
+
+#if defined(LIBPOOL_THREAD_SAFE)
+ if (!pool_ext_mutex_lock(&pool->lock)) {
+ LIBPOOL_LOG("Failed to lock mutex.");
+ return NULL;
+ }
+#endif /* defined(LIBPOOL_THREAD_SAFE) */
+
VALGRIND_MAKE_MEM_DEFINED(pool, sizeof(Pool));
if (pool->free_chunk == NULL) {
LIBPOOL_LOG("No free chunks in pool.");
- return NULL;
+ goto done;
}
VALGRIND_MAKE_MEM_DEFINED(pool->free_chunk, sizeof(void**));
@@ -295,6 +374,11 @@ void* pool_alloc(Pool* pool) {
VALGRIND_MAKE_MEM_NOACCESS(pool->free_chunk, sizeof(void**));
VALGRIND_MAKE_MEM_NOACCESS(pool, sizeof(Pool));
+done:
+#if defined(LIBPOOL_THREAD_SAFE)
+ pool_ext_mutex_unlock(&pool->lock);
+#endif /* defined(LIBPOOL_THREAD_SAFE) */
+
return result;
}
@@ -308,6 +392,13 @@ void pool_free(Pool* pool, void* ptr) {
return;
}
+#if defined(LIBPOOL_THREAD_SAFE)
+ if (!pool_ext_mutex_lock(&pool->lock)) {
+ LIBPOOL_LOG("Failed to lock mutex.");
+ return;
+ }
+#endif /* defined(LIBPOOL_THREAD_SAFE) */
+
VALGRIND_MAKE_MEM_DEFINED(pool, sizeof(Pool));
*(void**)ptr = pool->free_chunk;
@@ -315,4 +406,8 @@ void pool_free(Pool* pool, void* ptr) {
VALGRIND_MAKE_MEM_NOACCESS(pool, sizeof(Pool));
VALGRIND_MEMPOOL_FREE(pool, ptr);
+
+#if defined(LIBPOOL_THREAD_SAFE)
+ pool_ext_mutex_unlock(&pool->lock);
+#endif /* defined(LIBPOOL_THREAD_SAFE) */
}
diff --git a/src/libpool.h b/src/libpool.h
index e80d0bc..54bd816 100644
--- a/src/libpool.h
+++ b/src/libpool.h
@@ -37,6 +37,37 @@ typedef void (*PoolFreeFuncPtr)(void*);
extern PoolAllocFuncPtr pool_ext_alloc;
extern PoolFreeFuncPtr pool_ext_free;
+/*
+ * External functions for thread-safe operations, only used if
+ * 'LIBPOOL_THEAD_SAFE' is defined.
+ *
+ * If `LIBPOOL_NO_STDLIB' is defined, they are set to NULL, so the user must
+ * initialize them. Otherwise, their default value are POSIX Thread (pthread)
+ * functions.
+ */
+#if defined(LIBPOOL_THREAD_SAFE)
+#if defined(LIBPOOL_NO_STDLIB)
+#if !defined(POOL_EXT_MUTEX_TYPE)
+#error \
+ "POOL_EXT_MUTEX_TYPE must be defined if LIBPOOL_THREAD_SAFE and LIBPOOL_NO_STDLIB are defined."
+#endif /* !defined(POOL_EXT_MUTEX_TYPE) */
+typedef POOL_EXT_MUTEX_TYPE pool_ext_mutex_t;
+#else /* !defined(LIBPOOL_NO_STDLIB) */
+#include
+typedef pthread_mutex_t pool_ext_mutex_t;
+#endif /* !defined(LIBPOOL_NO_STDLIB) */
+typedef bool (*PoolMutexInitFuncPtr)(pool_ext_mutex_t*);
+typedef bool (*PoolMutexLockFuncPtr)(pool_ext_mutex_t*);
+typedef bool (*PoolMutexUnlockFuncPtr)(pool_ext_mutex_t*);
+typedef bool (*PoolMutexDestroyFuncPtr)(pool_ext_mutex_t*);
+extern PoolMutexInitFuncPtr pool_ext_mutex_init;
+extern PoolMutexLockFuncPtr pool_ext_mutex_lock;
+extern PoolMutexUnlockFuncPtr pool_ext_mutex_unlock;
+extern PoolMutexDestroyFuncPtr pool_ext_mutex_destroy;
+#endif /* defined(LIBPOOL_THREAD_SAFE) */
+
+/*----------------------------------------------------------------------------*/
+
/*
* Allocate and initialize a new `Pool' structure, with the specified number of
* chunks, each with the specified size.