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.