diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml new file mode 100644 index 0000000..fb1af23 --- /dev/null +++ b/.github/workflows/tests.yml @@ -0,0 +1,26 @@ +name: Tests + +on: + push: + pull_request: + +jobs: + test-normal-compile: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - name: Install Valgrind + run: sudo apt-get install -y valgrind + - name: Run tests with standard compilation flags + working-directory: tests/ + run: ./normal_compile.sh + + test-optimized-compile: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - name: Install Valgrind + run: sudo apt-get install -y valgrind + - name: Run tests with optimized compilation flags + working-directory: tests/ + run: ./optimized_compile.sh diff --git a/README.md b/README.md index 694a56a..051d179 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,5 @@ -![Build status](http://178.62.170.124:3000/pithikos/c-thread-pool/badge/?branch=master) +[![GitHub Actions](https://github.com/Pithikos/C-Thread-Pool/workflows/tests.yml/badge.svg?branch=master)](https://github.com/Pithikos/C-Thread-Pool/actions?query=workflow%3Atests+branch%3Amaster) + # C Thread Pool @@ -65,3 +66,5 @@ You are very welcome to contribute. If you have a new feature in mind, you can a * For coding style simply try to stick to the conventions you find in the existing codebase. * Tests: A new fix or feature should be covered by tests. If the existing tests are not sufficient, we expect an according test to follow with the pull request. * Documentation: for a new feature please add documentation. For an API change the documentation has to be thorough and super easy to understand. + +If you wish to **get access as a collaborator** feel free to mention it in the issue https://github.com/Pithikos/C-Thread-Pool/issues/78 diff --git a/docs/Design.md b/docs/Design.md index 68450ad..c45394b 100644 --- a/docs/Design.md +++ b/docs/Design.md @@ -1,47 +1,47 @@ ## High level - + Description: Library providing a threading pool where you can add work on the fly. The number of threads in the pool is adjustable when creating the pool. In most cases this should equal the number of threads supported by your cpu. - + For an example on how to use the threadpool, check the main.c file or just read the documentation found in the README.md file. - + In this header file a detailed overview of the functions and the threadpool's logical - scheme is presented in case you wish to tweak or alter something. - - - - _______________________________________________________ + scheme is presented in case you wish to tweak or alter something. + + + + _______________________________________________________ / \ - | JOB QUEUE | job1 | job2 | job3 | job4 | .. | + | JOB QUEUE | job1 | job2 | job3 | job4 | .. | | | | threadpool | thread1 | thread2 | .. | \_______________________________________________________/ - - + + Description: Jobs are added to the job queue. Once a thread in the pool - is idle, it is assigned with the first job from the queue(and - erased from the queue). It's each thread's job to read from - the queue serially(using lock) and executing each job + is idle, it is assigned the first job from the queue (and that job is + erased from the queue). It is each thread's job to read from + the queue serially (using lock) and executing each job until the queue is empty. - - + + Scheme: - - thpool______ jobqueue____ ______ + + thpool______ jobqueue____ ______ | | | | .----------->|_job0_| Newly added job | | | rear ----------' |_job1_| | jobqueue----------------->| | |_job2_| - | | | front ----------. |__..__| + | | | front ----------. |__..__| |___________| |___________| '----------->|_jobn_| Job for thread to take - - - job0________ + + + job0________ | | | function----> | | | arg-------> - | | job1________ + | | job1________ | next-------------->| | |___________| | |.. diff --git a/docs/FAQ.md b/docs/FAQ.md index a82c3f0..0af0589 100644 --- a/docs/FAQ.md +++ b/docs/FAQ.md @@ -1,7 +1,7 @@ +### Why isn't `pthread_exit()` used to exit a thread? -###Why isn't pthread_exit() used to exit a thread? -`thread_do` used to use pthread_exit(). However that resulted in -hard times of testing for memory leaks. The reason is that on pthread_exit() +`thread_do` used to use `pthread_exit()`. However that resulted in +hard times of testing for memory leaks. The reason is that on `pthread_exit()` not all memory is freed bt pthread (probably for future threads or false belief that the application is terminating). For these reasons a simple return is used. @@ -9,27 +9,28 @@ is used. Interestingly using `pthread_exit()` results in much more memory being allocated. -###Why do you use sleep() after calling thpool_destroy()? -This is needed only in the tests. The reason is that if you call thpool_destroy -and then exit immedietely, maybe the program will exit before all the threads +### Why do you use `sleep()` after calling `thpool_destroy()`? + +This is needed only in the tests. The reason is that if you call `thpool_destroy()` +and then exit immediately, maybe the program will exit before all the threads had the time to deallocate. In that way it is impossible to check for memory leaks. -In production you don't have to worry about this since if you call exit, -immedietely after you destroyied the pool, the threads will be freed -anyway by the OS. If you eitherway destroy the pool in the middle of your +In production you don't have to worry about this since if you call `exit()`, +immediately after you destroyed the pool, the threads will be freed +anyway by the OS. If you anyway destroy the pool in the middle of your program it doesn't matter again since the program will not exit immediately and thus threads will have more than enough time to terminate. +### Why does `wait()` use all my CPU? -###Why does wait() use all my CPU? -Notice: As of 11-Dec-2015 wait() doesn't use polling anymore. Instead a conditional variable is being used so in theory there should not be any CPU overhead. +Notice: As of 11-Dec-2015 `wait()` doesn't use polling anymore. Instead a conditional variable is being used so in theory there should not be any CPU overhead. Normally `wait()` will spike CPU usage to full when called. This is normal as long as it doesn't last for more than 1 second. The reason this happens is that `wait()` goes through various phases of polling (what is called smart polling). * Initially there is no interval between polling and hence the 100% use of your CPU. * After that the polling interval grows exponentially. * Finally after x seconds, if there is still work, polling falls back to a very big interval. - + The reason `wait()` works in this way, is that the function is mostly used when someone wants to wait for some calculation to finish. So if the calculation is assumed to take a long time then we don't want to poll too often. Still we want to poll fast in case the calculation is a simple one. To solve these two problems, this seemingly awkward behaviour is present. diff --git a/example.c b/example.c index 0ac638f..22ea241 100644 --- a/example.c +++ b/example.c @@ -13,16 +13,11 @@ #include #include +#include #include "thpool.h" - -void task1(){ - printf("Thread #%u working on task1\n", (int)pthread_self()); -} - - -void task2(){ - printf("Thread #%u working on task2\n", (int)pthread_self()); +void task(void *arg){ + printf("Thread #%u working on %d\n", (int)pthread_self(), (int) arg); } @@ -33,11 +28,11 @@ int main(){ puts("Adding 40 tasks to threadpool"); int i; - for (i=0; i<20; i++){ - thpool_add_work(thpool, (void*)task1, NULL); - thpool_add_work(thpool, (void*)task2, NULL); + for (i=0; i<40; i++){ + thpool_add_work(thpool, task, (void*)(uintptr_t)i); }; + thpool_wait(thpool); puts("Killing threadpool"); thpool_destroy(thpool); diff --git a/tests/memleaks.sh b/tests/memleaks.sh index b85dac3..2869155 100755 --- a/tests/memleaks.sh +++ b/tests/memleaks.sh @@ -28,25 +28,48 @@ function test_thread_free { #threads } +function _test_thread_free_multi { #threads + output=$(valgrind --leak-check=full --track-origins=yes ./test "$1" 2>&1 > /dev/null) + heap_usage=$(echo "$output" | grep "total heap usage") + allocs=$(extract_num "[0-9]* allocs" "$heap_usage") + frees=$(extract_num "[0-9]* frees" "$heap_usage") + if (( "$allocs" == 0 )); then + err "Allocated 0 times. Something is wrong.." "$output" + return 1 + fi + if (( "$allocs" != "$frees" )); then + err "Allocated $allocs times but freed only $frees" "$output" + return 1 + fi + #echo "Allocs: $allocs Frees: $frees" +} + + # This is the same with test_many_thread_allocs but multiplied -function test_thread_free_multi { #threads #times +function test_thread_free_multi { #threads #times #nparallel echo "Testing multiple threads creation and destruction in pool(threads=$1 times=$2)" compile src/no_work.c - for ((i = 1; i <= $2; i++)); do + pids=() + nparallel="${3:-10}" + + # Run tests in p + for (( i = 1; i <= "$2"; i++ )); do + + # Run test in background python -c "import sys; sys.stdout.write('$i/$2\r')" - output=$(valgrind --leak-check=full --track-origins=yes ./test "$1" 2>&1 > /dev/null) - heap_usage=$(echo "$output" | grep "total heap usage") - allocs=$(extract_num "[0-9]* allocs" "$heap_usage") - frees=$(extract_num "[0-9]* frees" "$heap_usage") - if (( "$allocs" == 0 )); then - err "Allocated 0 times. Something is wrong.." "$output" - exit 1 - fi - if (( "$allocs" != "$frees" )); then - err "Allocated $allocs times but freed only $frees" "$output" - exit 1 + _test_thread_free_multi "$1" & + pids+=($!) + + # Wait for 10 background jobs to finish + if (( "$i" % 10 == 0 )); then + for pid in ${pids[@]}; do + wait $pid + if (( $? != 0 )); then + err "Test failed" "Test failed" + fi + done + pids=() fi - #echo "Allocs: $allocs Frees: $frees" done echo } @@ -62,7 +85,12 @@ test_thread_free 8 test_thread_free 1 test_thread_free 20 test_thread_free_multi 4 20 -test_thread_free_multi 3 1000 -test_thread_free_multi 100 100 + +# test_thread_free_multi 3 1000 # Takes way too long +test_thread_free_multi 3 200 + +# test_thread_free_multi 100 100 # Takes way too long +test_thread_free_multi 100 20 1 + echo "No memory leaks" diff --git a/tests/src/conc_increment.c b/tests/src/conc_increment.c index 5caf762..21034e3 100644 --- a/tests/src/conc_increment.c +++ b/tests/src/conc_increment.c @@ -20,7 +20,7 @@ int main(int argc, char *argv[]){ char* p; if (argc != 3){ - puts("This testfile needs excactly two arguments"); + puts("This testfile needs exactly two arguments"); exit(1); } int num_jobs = strtol(argv[1], &p, 10); diff --git a/tests/src/pause_resume.c b/tests/src/pause_resume.c index 282c398..d96689a 100644 --- a/tests/src/pause_resume.c +++ b/tests/src/pause_resume.c @@ -24,7 +24,7 @@ int main(int argc, char *argv[]){ char* p; if (argc != 2){ - puts("This testfile needs excactly one arguments"); + puts("This testfile needs exactly one arguments"); exit(1); } int num_threads = strtol(argv[1], &p, 10); diff --git a/thpool.c b/thpool.c index a781308..4f260a5 100644 --- a/thpool.c +++ b/thpool.c @@ -8,7 +8,16 @@ * ********************************/ +#if defined(__APPLE__) +#include +#else +#ifndef _POSIX_C_SOURCE #define _POSIX_C_SOURCE 200809L +#endif +#ifndef _XOPEN_SOURCE +#define _XOPEN_SOURCE 500 +#endif +#endif #include #include #include @@ -19,6 +28,9 @@ #if defined(__linux__) #include #endif +#if defined(__FreeBSD__) || defined(__OpenBSD__) +#include +#endif #include "thpool.h" @@ -34,6 +46,13 @@ #define err(str) #endif +#ifndef THPOOL_THREAD_NAME +#define THPOOL_THREAD_NAME thpool +#endif + +#define STRINGIFY(x) #x +#define TOSTRING(x) STRINGIFY(x) + static volatile int threads_on_hold; /* ========================== STRUCTURES ============================ */ @@ -203,7 +222,7 @@ void thpool_wait(thpool_* thpool_p){ /* Destroy the threadpool */ void thpool_destroy(thpool_* thpool_p){ - /* No need to destory if it's NULL */ + /* No need to destroy if it's NULL */ if (thpool_p == NULL) return ; volatile int threads_total = thpool_p->num_threads_alive; @@ -252,7 +271,7 @@ void thpool_pause(thpool_* thpool_p) { /* Resume all threads in threadpool */ void thpool_resume(thpool_* thpool_p) { // resuming a single threadpool hasn't been - // implemented yet, meanwhile this supresses + // implemented yet, meanwhile this suppresses // the warnings (void)thpool_p; @@ -280,7 +299,7 @@ int thpool_num_threads_working(thpool_* thpool_p){ static int thread_init (thpool_* thpool_p, struct thread** thread_p, int id){ *thread_p = (struct thread*)malloc(sizeof(struct thread)); - if (thread_p == NULL){ + if (*thread_p == NULL){ err("thread_init(): Could not allocate memory for thread\n"); return -1; } @@ -288,7 +307,7 @@ static int thread_init (thpool_* thpool_p, struct thread** thread_p, int id){ (*thread_p)->thpool_p = thpool_p; (*thread_p)->id = id; - pthread_create(&(*thread_p)->pthread, NULL, (void *)thread_do, (*thread_p)); + pthread_create(&(*thread_p)->pthread, NULL, (void * (*)(void *)) thread_do, (*thread_p)); pthread_detach((*thread_p)->pthread); return 0; } @@ -306,7 +325,7 @@ static void thread_hold(int sig_id) { /* What each thread is doing * -* In principle this is an endless loop. The only time this loop gets interuppted is once +* In principle this is an endless loop. The only time this loop gets interrupted is once * thpool_destroy() is invoked or the program exits. * * @param thread thread that will run this function @@ -314,15 +333,18 @@ static void thread_hold(int sig_id) { */ static void* thread_do(struct thread* thread_p){ - /* Set thread name for profiling and debuging */ - char thread_name[128] = {0}; - sprintf(thread_name, "thread-pool-%d", thread_p->id); + /* Set thread name for profiling and debugging */ + char thread_name[16] = {0}; + + snprintf(thread_name, 16, TOSTRING(THPOOL_THREAD_NAME) "-%d", thread_p->id); #if defined(__linux__) /* Use prctl instead to prevent using _GNU_SOURCE flag and implicit declaration */ prctl(PR_SET_NAME, thread_name); #elif defined(__APPLE__) && defined(__MACH__) pthread_setname_np(thread_name); +#elif defined(__FreeBSD__) || defined(__OpenBSD__) + pthread_set_name_np(thread_p->pthread, thread_name); #else err("thread_do(): pthread_setname_np is not supported on this system"); #endif @@ -333,7 +355,7 @@ static void* thread_do(struct thread* thread_p){ /* Register signal handler */ struct sigaction act; sigemptyset(&act.sa_mask); - act.sa_flags = 0; + act.sa_flags = SA_ONSTACK; act.sa_handler = thread_hold; if (sigaction(SIGUSR1, &act, NULL) == -1) { err("thread_do(): cannot handle SIGUSR1"); @@ -454,11 +476,7 @@ static void jobqueue_push(jobqueue* jobqueue_p, struct job* newjob){ /* Get first job from queue(removes it from queue) -<<<<<<< HEAD - * * Notice: Caller MUST hold a mutex -======= ->>>>>>> da2c0fe45e43ce0937f272c8cd2704bdc0afb490 */ static struct job* jobqueue_pull(jobqueue* jobqueue_p){ @@ -516,6 +534,8 @@ static void bsem_init(bsem *bsem_p, int value) { /* Reset semaphore to 0 */ static void bsem_reset(bsem *bsem_p) { + pthread_mutex_destroy(&(bsem_p->mutex)); + pthread_cond_destroy(&(bsem_p->cond)); bsem_init(bsem_p, 0); } diff --git a/thpool.h b/thpool.h index 04d5af2..af3e68d 100644 --- a/thpool.h +++ b/thpool.h @@ -20,7 +20,7 @@ typedef struct thpool_* threadpool; /** * @brief Initialize threadpool * - * Initializes a threadpool. This function will not return untill all + * Initializes a threadpool. This function will not return until all * threads have initialized successfully. * * @example @@ -62,7 +62,7 @@ threadpool thpool_init(int num_threads); * @param threadpool threadpool to which the work will be added * @param function_p pointer to function to add as work * @param arg_p pointer to an argument - * @return 0 on successs, -1 otherwise. + * @return 0 on success, -1 otherwise. */ int thpool_add_work(threadpool, void (*function_p)(void*), void* arg_p); @@ -77,7 +77,7 @@ int thpool_add_work(threadpool, void (*function_p)(void*), void* arg_p); * Smart polling is used in wait. The polling is initially 0 - meaning that * there is virtually no polling at all. If after 1 seconds the threads * haven't finished, the polling interval starts growing exponentially - * untill it reaches max_secs seconds. Then it jumps down to a maximum polling + * until it reaches max_secs seconds. Then it jumps down to a maximum polling * interval assuming that heavy processing is being used in the threadpool. * * @example