| /*****************************************************************************\ | |
| * threadpool.c - Thread Pool | |
| ***************************************************************************** | |
| * Copyright (C) SchedMD LLC. | |
| * | |
| * This file is part of Slurm, a resource management program. | |
| * For details, see <https://slurm.schedmd.com/>. | |
| * Please also read the included file: DISCLAIMER. | |
| * | |
| * Slurm 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 2 of the License, or (at your option) | |
| * any later version. | |
| * | |
| * In addition, as a special exception, the copyright holders give permission | |
| * to link the code of portions of this program with the OpenSSL library under | |
| * certain conditions as described in each individual source file, and | |
| * distribute linked combinations including the two. You must obey the GNU | |
| * General Public License in all respects for all of the code used other than | |
| * OpenSSL. If you modify file(s) with this exception, you may extend this | |
| * exception to your version of the file(s), but you are not obligated to do | |
| * so. If you do not wish to do so, delete this exception statement from your | |
| * version. If you delete this exception statement from all source files in | |
| * the program, then also delete it here. | |
| * | |
| * Slurm 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 Slurm; if not, write to the Free Software Foundation, Inc., | |
| * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. | |
| \*****************************************************************************/ | |
| #include "config.h" | |
| #if HAVE_SYS_PRCTL_H | |
| #include <sys/prctl.h> | |
| #endif | |
| #include <pthread.h> | |
| #include <stdint.h> | |
| #include "src/common/events.h" | |
| #include "src/common/list.h" | |
| #include "src/common/log.h" | |
| #include "src/common/macros.h" | |
| #include "src/common/probes.h" | |
| #include "src/common/read_config.h" | |
| #include "src/common/slurm_time.h" | |
| #include "src/common/threadpool.h" | |
| #include "src/common/timers.h" | |
| #include "src/common/xassert.h" | |
| #include "src/common/xmalloc.h" | |
| #include "src/common/xstring.h" | |
| /* | |
| * From man prctl: | |
| * If the length of the string, including the terminating null byte, | |
| * exceeds 16 bytes, the string is silently truncated. | |
| */ | |
| #define PRCTL_BUF_BYTES 17 | |
| /* default thread name for logging */ | |
| #define CTIME_STR_LEN 72 | |
| /* | |
| * Avoid compiler warnings for id not fitting in PRCTL_BUF_BYTES by using %hu | |
| * and uint16_t instead of %d. This risks the index rolling over on longer lived | |
| * daemons. | |
| */ | |
| #define THREAD_NAME_FMT "worker[%hu]" | |
| #ifdef PTHREAD_SCOPE_SYSTEM | |
| #define slurm_attr_init(attr) \ | |
| do { \ | |
| int err = pthread_attr_init(attr); \ | |
| if (err) { \ | |
| errno = err; \ | |
| fatal("pthread_attr_init: %m"); \ | |
| } \ | |
| /* we want 1:1 threads if there is a choice */ \ | |
| err = pthread_attr_setscope(attr, PTHREAD_SCOPE_SYSTEM); \ | |
| if (err) { \ | |
| errno = err; \ | |
| error("pthread_attr_setscope: %m"); \ | |
| } \ | |
| err = pthread_attr_setstacksize(attr, STACK_SIZE); \ | |
| if (err) { \ | |
| errno = err; \ | |
| error("pthread_attr_setstacksize: %m"); \ | |
| } \ | |
| } while (0) | |
| #else | |
| #define slurm_attr_init(attr) \ | |
| do { \ | |
| int err = pthread_attr_init(attr); \ | |
| if (err) { \ | |
| errno = err; \ | |
| fatal("pthread_attr_init: %m"); \ | |
| } \ | |
| err = pthread_attr_setstacksize(attr, STACK_SIZE); \ | |
| if (err) { \ | |
| errno = err; \ | |
| error("pthread_attr_setstacksize: %m"); \ | |
| } \ | |
| } while (0) | |
| #endif | |
| typedef enum { | |
| THREAD_STATE_INVALID = 0, | |
| THREAD_STATE_NEW, | |
| THREAD_STATE_CLONING, | |
| /* pending assignment of hardware thread */ | |
| THREAD_STATE_PENDING, | |
| THREAD_STATE_ASSIGNED, | |
| /* Waiting for requester to acknowledge assignment */ | |
| THREAD_STATE_ASSIGNED_WAIT, | |
| /* Requester acknowledged assignment */ | |
| THREAD_STATE_ASSIGNED_ACK, | |
| THREAD_STATE_RUNNING, | |
| /* Waiting for join() */ | |
| THREAD_STATE_ZOMBIE, | |
| THREAD_STATE_COMPLETE, | |
| THREAD_STATE_INVALID_MAX | |
| } thread_state_t; | |
| #define T(x, str) [x] = { x, str } | |
| static const struct { | |
| thread_state_t state; | |
| const char *str; | |
| } thread_states[] = { | |
| T(THREAD_STATE_NEW, "NEW"), | |
| T(THREAD_STATE_CLONING, "CLONING"), | |
| T(THREAD_STATE_PENDING, "PENDING"), | |
| T(THREAD_STATE_ASSIGNED, "ASSIGNED"), | |
| T(THREAD_STATE_ASSIGNED_WAIT, "ASSIGNED_WAIT"), | |
| T(THREAD_STATE_ASSIGNED_ACK, "ASSIGNED_ACK"), | |
| T(THREAD_STATE_RUNNING, "RUNNING"), | |
| T(THREAD_STATE_ZOMBIE, "ZOMBIE"), | |
| T(THREAD_STATE_COMPLETE, "COMPLETE"), | |
| }; | |
| #define THREAD_MAGIC 0xA434F4D2 | |
| typedef struct { | |
| int magic; /* THREAD_MAGIC */ | |
| thread_state_t state; | |
| /* pthread_self() from thread */ | |
| pthread_t id; | |
| bool detached; | |
| /* thread waiting for assignment */ | |
| pthread_t requester; | |
| threadpool_func_t func; | |
| timespec_t requested; | |
| const char *thread_name; | |
| const char *func_name; | |
| void *arg; | |
| /* return from func() */ | |
| void *ret; | |
| } thread_t; | |
| #define LOG_THREAD_MAGIC 0xabbb1119 | |
| typedef struct { | |
| int magic; /* LOG_THREAD_MAGIC */ | |
| const char *type; /* zombie/pending */ | |
| probe_log_t *log; | |
| } log_thread_t; | |
| static struct { | |
| /* true if enabled, false to ignore everything else */ | |
| bool enabled; | |
| /* true if shutdown requested */ | |
| bool shutdown; | |
| /* list_t * of thread_t * */ | |
| list_t *pending; | |
| /* Count of threads currently in THREAD_STATE_ZOMBIE */ | |
| int zombies; | |
| /* list_t * of thread_t * */ | |
| list_t *attached; | |
| /* Number of running threads */ | |
| int running; | |
| /* Number of idle threads */ | |
| int idle; | |
| /* counter of the threads run */ | |
| uint64_t total_run; | |
| /* counter of the threads created */ | |
| uint64_t total_created; | |
| /* peak thead count encountered */ | |
| uint64_t peak_count; | |
| pthread_mutex_t mutex; | |
| struct { | |
| event_signal_t assign; | |
| event_signal_t assigned; | |
| event_signal_t assigned_ack; | |
| event_signal_t detach; | |
| event_signal_t end; | |
| } events; | |
| struct { | |
| /* histogram of the latency from request to run */ | |
| latency_histogram_t request; | |
| /* histogram of the time to run func() in threads */ | |
| latency_histogram_t run; | |
| /* histogram of the latency to join threads */ | |
| latency_histogram_t join; | |
| } histograms; | |
| struct { | |
| int preallocate; | |
| int preserve; | |
| } config; | |
| } threadpool = { | |
| .enabled = false, | |
| .mutex = PTHREAD_MUTEX_INITIALIZER, | |
| .events = { | |
| .assign = EVENT_INITIALIZER("THREADPOOL-ASSIGN-THREAD"), | |
| .assigned = EVENT_INITIALIZER("THREADPOOL-ASSIGNED-THREAD"), | |
| .assigned_ack = EVENT_INITIALIZER("THREADPOOL-ASSIGNED-ACK-THREAD"), | |
| .detach = EVENT_INITIALIZER("THREADPOOL-DETACH-THREAD"), | |
| .end = EVENT_INITIALIZER("THREADPOOL-END-THREAD"), | |
| }, | |
| .histograms = { | |
| .request = LATENCY_HISTOGRAM_INITIALIZER, | |
| .run = LATENCY_HISTOGRAM_INITIALIZER, | |
| .join = LATENCY_HISTOGRAM_INITIALIZER, | |
| }, | |
| .config = { | |
| .preallocate = THREADPOOL_DEFAULT_PREALLOCATE, | |
| .preserve = THREADPOOL_DEFAULT_PRESERVE, | |
| }, | |
| }; | |
| /* Define slurm-specific aliases for use by plugins, see slurm_xlator.h. */ | |
| strong_alias(threadpool_create, slurm_threadpool_create); | |
| strong_alias(threadpool_join, slurm_threadpool_join); | |
| strong_alias(threadpool_init, slurm_threadpool_init); | |
| strong_alias(threadpool_fini, slurm_threadpool_fini); | |
| static const char *_thread_state_str(const thread_state_t state) | |
| { | |
| xassert(state > THREAD_STATE_INVALID); | |
| xassert(state < THREAD_STATE_INVALID_MAX); | |
| return thread_states[state].str; | |
| } | |
| static int _match_thread_id(void *x, void *key) | |
| { | |
| const thread_t *thread = x; | |
| const pthread_t *id_ptr = key; | |
| xassert(thread->magic == THREAD_MAGIC); | |
| xassert(thread->state > THREAD_STATE_INVALID); | |
| xassert(thread->state < THREAD_STATE_INVALID_MAX); | |
| xassert(*id_ptr > 0); | |
| return (pthread_equal(thread->id, *id_ptr) ? 1 : 0); | |
| } | |
| #ifndef NDEBUG | |
| static int _match_thread_ptr(void *x, void *y) | |
| { | |
| thread_t *thread = x; | |
| thread_t *key = y; | |
| xassert(thread->magic == THREAD_MAGIC); | |
| xassert(thread->state > THREAD_STATE_INVALID); | |
| xassert(thread->state < THREAD_STATE_INVALID_MAX); | |
| xassert(key->magic == THREAD_MAGIC); | |
| return ((thread == key) ? 1 : 0); | |
| } | |
| #endif | |
| static int _join(const pthread_t id, const char *caller) | |
| { | |
| int rc = EINVAL; | |
| void *ret = NULL; | |
| if ((rc = pthread_join(id, &ret))) { | |
| error("%s->%s: pthread_join(id=0x%"PRIx64") failed: %s", | |
| caller, __func__, (uint64_t) id, slurm_strerror(rc)); | |
| return rc; | |
| } | |
| if (ret == PTHREAD_CANCELED) | |
| log_flag(THREAD, "%s->%s: pthread id=0x%"PRIx64" was cancelled", | |
| caller, __func__, (uint64_t) id); | |
| else | |
| log_flag(THREAD, "%s->%s: pthread id=0x%"PRIx64" returned: 0x%"PRIxPTR, | |
| caller, __func__, (uint64_t) id, (uintptr_t) ret); | |
| return rc; | |
| } | |
| static int _threadpool_on_detach(thread_t *thread, const char *caller) | |
| { | |
| log_flag(THREAD, "%s->%s: detached pthread id=0x%"PRIx64, | |
| caller, __func__, (uint64_t) thread->id); | |
| if (!list_delete_ptr(threadpool.attached, thread)) | |
| fatal_abort("this should never happen"); | |
| if (thread->state == THREAD_STATE_ZOMBIE) { | |
| xassert(threadpool.zombies > 0); | |
| threadpool.zombies--; | |
| } | |
| xassert(!thread->detached); | |
| thread->detached = true; | |
| EVENT_BROADCAST(&threadpool.events.detach); | |
| return SLURM_SUCCESS; | |
| } | |
| static int _threadpool_join(const pthread_t id, const char *caller) | |
| { | |
| thread_t *thread = NULL; | |
| const timespec_t start_ts = timespec_now(); | |
| slurm_mutex_lock(&threadpool.mutex); | |
| while ((thread = list_find_first(threadpool.attached, _match_thread_id, | |
| (void *) &id))) { | |
| xassert(thread->magic == THREAD_MAGIC); | |
| xassert(thread->state > THREAD_STATE_NEW); | |
| xassert(thread->state < THREAD_STATE_COMPLETE); | |
| xassert(!thread->detached); | |
| if (thread->state > THREAD_STATE_RUNNING) { | |
| const int rc = _threadpool_on_detach(thread, caller); | |
| log_flag(THREAD, "%s->%s: joined %s thread id=0x%"PRIx64" returned: 0x%"PRIxPTR, | |
| caller, __func__, | |
| _thread_state_str(thread->state), | |
| (uint64_t) thread->id, | |
| (uintptr_t) thread->ret); | |
| slurm_mutex_unlock(&threadpool.mutex); | |
| HISTOGRAM_ADD_DURATION(&threadpool.histograms.join, | |
| start_ts); | |
| return rc; | |
| } | |
| log_flag(THREAD, "%s->%s: waiting for %s thread id=0x%"PRIx64" with %d running threads after %s", | |
| caller, __func__, | |
| _thread_state_str(thread->state), (uint64_t) id, | |
| threadpool.running, | |
| TIMESPEC_ELAPSED_STR(start_ts)); | |
| EVENT_WAIT(&threadpool.events.detach, &threadpool.mutex); | |
| } | |
| log_flag(THREAD, "%s->%s: pthread id=0x%"PRIx64" does not exist after %s", | |
| caller, __func__, (uint64_t) id, | |
| TIMESPEC_ELAPSED_STR(start_ts)); | |
| slurm_mutex_unlock(&threadpool.mutex); | |
| return ESRCH; | |
| } | |
| extern int threadpool_join(const pthread_t id, const char *caller) | |
| { | |
| if (!id) { | |
| log_flag(THREAD, "%s->%s: Ignoring invalid pthread id=0x0", | |
| caller, __func__); | |
| return SLURM_SUCCESS; | |
| } | |
| if (threadpool.enabled) | |
| return _threadpool_join(id, caller); | |
| else | |
| return _join(id, caller); | |
| } | |
| static const char *_thread_default_name(void) | |
| { | |
| static __thread char name[PRCTL_BUF_BYTES] = { 0 }; | |
| static pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER; | |
| static int thread_count = 0; | |
| uint16_t index = -1; | |
| if (name[0]) | |
| return name; | |
| slurm_mutex_lock(&mutex); | |
| index = thread_count++; | |
| slurm_mutex_unlock(&mutex); | |
| (void) snprintf(name, sizeof(name), THREAD_NAME_FMT, index); | |
| return name; | |
| } | |
| static const char *_thread_name(thread_t *thread) | |
| { | |
| const char *name = NULL; | |
| if (thread) | |
| name = thread->thread_name; | |
| if (!name) | |
| name = _thread_default_name(); | |
| return name; | |
| } | |
| static void _set_thread_name(thread_t *thread) | |
| { | |
| #if HAVE_SYS_PRCTL_H | |
| const char *name = _thread_name(thread); | |
| if (prctl(PR_SET_NAME, name, NULL, NULL, NULL)) | |
| error("%s: cannot set process name to %s %m", __func__, name); | |
| #endif | |
| } | |
| /* | |
| * Takes ownership of pointer. | |
| * Caller must not own threadpool.mutex lock. | |
| */ | |
| static void _thread_free(thread_t **thread_ptr) | |
| { | |
| thread_t *thread = *thread_ptr; | |
| *thread_ptr = NULL; | |
| if (!thread) | |
| return; | |
| #ifndef NDEBUG | |
| xassert(thread->magic == THREAD_MAGIC); | |
| if (threadpool.enabled) { | |
| /* All threads must be detached at cleanup */ | |
| xassert(thread->detached); | |
| xassert(thread->state == THREAD_STATE_COMPLETE); | |
| xassert(!thread->requester); | |
| slurm_mutex_lock(&threadpool.mutex); | |
| /* check for dangling pointers */ | |
| xassert(!list_find_first(threadpool.attached, _match_thread_ptr, | |
| thread)); | |
| xassert(!list_find_first(threadpool.pending, _match_thread_ptr, | |
| thread)); | |
| slurm_mutex_unlock(&threadpool.mutex); | |
| } | |
| #endif | |
| thread->magic = ~THREAD_MAGIC; | |
| thread->state = THREAD_STATE_INVALID_MAX; | |
| xfree(thread->thread_name); | |
| xfree(thread); | |
| } | |
| static void _threadpool_run(thread_t *thread) | |
| { | |
| const timespec_t start = timespec_now(); | |
| threadpool_func_t func = thread->func; | |
| void *arg = thread->arg; | |
| void *ret = NULL; | |
| xassert(thread->magic == THREAD_MAGIC); | |
| _set_thread_name(thread); | |
| log_flag(THREAD, "%s: [%s@0x%"PRIx64"] BEGIN: %s thread calling %s(0x%"PRIxPTR") after %s", | |
| __func__, _thread_name(thread), (uint64_t) thread->id, | |
| (thread->detached ? "detached" : "attached"), | |
| thread->func_name, (uintptr_t) thread->arg, | |
| TIMESPEC_DURATION_STR(thread->requested, start)); | |
| HISTOGRAM_ADD_DURATION(&threadpool.histograms.request, | |
| thread->requested); | |
| slurm_mutex_unlock(&threadpool.mutex); | |
| ret = func(arg); | |
| slurm_mutex_lock(&threadpool.mutex); | |
| thread->ret = ret; | |
| HISTOGRAM_ADD_DURATION(&threadpool.histograms.run, start); | |
| log_flag(THREAD, "%s: [%s@0x%"PRIx64"] END: %s thread called %s(0x%"PRIxPTR")=0x%"PRIxPTR" for %s", | |
| __func__, _thread_name(thread), (uint64_t) thread->id, | |
| (thread->detached ? "detached" : "attached"), | |
| thread->func_name, | |
| (uintptr_t) thread->arg, (uintptr_t) thread->ret, | |
| TIMESPEC_ELAPSED_STR(start)); | |
| } | |
| /* caller must hold threadpool.mutex lock */ | |
| static void _threadpool_wait_ack(thread_t *thread) | |
| { | |
| const timespec_t start_ts = timespec_now(); | |
| pthread_t requester = thread->requester; | |
| xassert(thread->state == THREAD_STATE_ASSIGNED); | |
| thread->state = THREAD_STATE_ASSIGNED_WAIT; | |
| log_flag(THREAD, "%s: [%s@0x%"PRIx64"] BEGIN: waiting for requester 0x%"PRIx64" to acknowledge assignment", | |
| __func__, _thread_name(thread), (uint64_t) thread->id, | |
| (uint64_t) requester); | |
| while (thread->requester) | |
| EVENT_WAIT(&threadpool.events.assigned_ack, &threadpool.mutex); | |
| xassert(thread->state == THREAD_STATE_ASSIGNED_ACK); | |
| log_flag(THREAD, "%s: [%s@0x%"PRIx64"] END: acknowledged by requester 0x%"PRIx64" after %s", | |
| __func__, _thread_name(thread), (uint64_t) thread->id, | |
| (uint64_t) requester, TIMESPEC_ELAPSED_STR(start_ts)); | |
| } | |
| /* caller must hold threadpool.mutex lock */ | |
| static void _threadpool_zombie(thread_t *thread) | |
| { | |
| const timespec_t start_ts = timespec_now(); | |
| log_flag(THREAD, "%s: [%s@0x%"PRIx64"] BEGIN: waiting to be joined", | |
| __func__, _thread_name(thread), (uint64_t) thread->id); | |
| /* Thread must exist in the attached list until detached */ | |
| xassert(list_find_first(threadpool.attached, _match_thread_ptr, | |
| thread)); | |
| xassert(thread->state == THREAD_STATE_RUNNING); | |
| thread->state = THREAD_STATE_ZOMBIE; | |
| threadpool.zombies++; | |
| xassert(threadpool.zombies > 0); | |
| EVENT_BROADCAST(&threadpool.events.detach); | |
| while (!thread->detached) | |
| EVENT_WAIT(&threadpool.events.detach, &threadpool.mutex); | |
| /* Thread must not exist in the attached list after being detached */ | |
| xassert(!list_find_first(threadpool.attached, _match_thread_ptr, | |
| thread)); | |
| log_flag(THREAD, "%s: [%s@0x%"PRIx64"] END: joined after waiting %s", | |
| __func__, _thread_name(thread), | |
| (uint64_t) thread->id, TIMESPEC_ELAPSED_STR(start_ts)); | |
| } | |
| /* caller must hold threadpool.mutex lock */ | |
| static void _threadpool_prerun(thread_t *thread) | |
| { | |
| xassert(thread->magic == THREAD_MAGIC); | |
| xassert(!thread->id); | |
| xassert((thread->state == THREAD_STATE_PENDING) || | |
| (thread->state == THREAD_STATE_CLONING)); | |
| thread->id = pthread_self(); | |
| thread->state = THREAD_STATE_ASSIGNED; | |
| threadpool.total_run++; | |
| threadpool.idle--; | |
| threadpool.running++; | |
| if ((threadpool.idle + threadpool.running) > threadpool.peak_count) | |
| threadpool.peak_count = (threadpool.idle + threadpool.running); | |
| xassert(threadpool.idle >= 0); | |
| xassert(threadpool.running > 0); | |
| EVENT_BROADCAST(&threadpool.events.assigned); | |
| if (thread->requester) | |
| _threadpool_wait_ack(thread); | |
| thread->state = THREAD_STATE_RUNNING; | |
| } | |
| /* caller must hold threadpool.mutex lock */ | |
| static void _threadpool_postrun(thread_t *thread) | |
| { | |
| xassert(thread->magic == THREAD_MAGIC); | |
| xassert(pthread_equal(thread->id, pthread_self())); | |
| xassert(thread->state == THREAD_STATE_RUNNING); | |
| threadpool.running--; | |
| threadpool.idle++; | |
| xassert(threadpool.idle > 0); | |
| xassert(threadpool.running >= 0); | |
| if (!thread->detached) | |
| _threadpool_zombie(thread); | |
| xassert(thread->magic == THREAD_MAGIC); | |
| xassert(thread->detached); | |
| xassert(!thread->requester); | |
| xassert(thread->id > 0); | |
| thread->state = THREAD_STATE_COMPLETE; | |
| thread->id = 0; | |
| } | |
| static void *_thread(void *arg) | |
| { | |
| thread_t *thread = arg; | |
| void *ret = NULL; | |
| const timespec_t start = timespec_now(); | |
| xassert(!threadpool.enabled); | |
| xassert(thread->magic == THREAD_MAGIC); | |
| xassert(thread->state == THREAD_STATE_CLONING); | |
| _set_thread_name(thread); | |
| thread->state = THREAD_STATE_RUNNING; | |
| log_flag(THREAD, "%s: [%s@0x%"PRIx64"] BEGIN: %s thread calling %s(0x%"PRIxPTR") after %s", | |
| __func__, _thread_name(thread), (uint64_t) thread->id, | |
| (thread->detached ? "detached" : "attached"), | |
| thread->func_name, (uintptr_t) thread->arg, | |
| TIMESPEC_DURATION_STR(thread->requested, start)); | |
| ret = thread->func(thread->arg); | |
| log_flag(THREAD, "%s: [%s@0x%"PRIx64"] END: %s thread called %s(0x%"PRIxPTR")=0x%"PRIxPTR" for %s", | |
| __func__, _thread_name(thread), (uint64_t) thread->id, | |
| (thread->detached ? "detached" : "attached"), | |
| thread->func_name, | |
| (uintptr_t) thread->arg, (uintptr_t) ret, | |
| TIMESPEC_ELAPSED_STR(start)); | |
| thread->state = THREAD_STATE_COMPLETE; | |
| _thread_free(&thread); | |
| return ret; | |
| } | |
| static void *_threadpool_thread(void *arg) | |
| { | |
| thread_t *thread = arg; | |
| xassert(threadpool.enabled); | |
| slurm_mutex_lock(&threadpool.mutex); | |
| threadpool.total_created++; | |
| threadpool.idle++; | |
| #ifndef NDEBUG | |
| if ((threadpool.running + threadpool.idle) > THREADPOOL_MAX_THREADS) | |
| warning("%s: threadpool is over capacity %d/%d", | |
| __func__, (threadpool.running + threadpool.idle), | |
| THREADPOOL_MAX_THREADS); | |
| #endif | |
| do { | |
| if (thread) { | |
| _threadpool_prerun(thread); | |
| _threadpool_run(thread); | |
| _threadpool_postrun(thread); | |
| slurm_mutex_unlock(&threadpool.mutex); | |
| _thread_free(&thread); | |
| slurm_mutex_lock(&threadpool.mutex); | |
| } | |
| if ((thread = list_pop(threadpool.pending))) | |
| continue; | |
| if (threadpool.shutdown) { | |
| log_flag(THREAD, "%s: [0x%"PRIx64"] exiting due to shutdown", | |
| __func__, (uint64_t) pthread_self()); | |
| break; | |
| } | |
| if (threadpool.idle > threadpool.config.preserve) { | |
| log_flag(THREAD, "%s: [0x%"PRIx64"] exiting due to %d/%d idle threads", | |
| __func__, (uint64_t) pthread_self(), | |
| threadpool.idle, threadpool.config.preserve); | |
| break; | |
| } | |
| log_flag(THREAD, "%s: [0x%"PRIx64"] waiting for pending thread work with %d/%d idle threads", | |
| __func__, (uint64_t) pthread_self(), threadpool.idle, | |
| threadpool.config.preserve); | |
| xassert(list_is_empty(threadpool.pending)); | |
| xassert(threadpool.idle > 0); | |
| EVENT_WAIT(&threadpool.events.assign, &threadpool.mutex); | |
| } while (true); | |
| threadpool.idle--; | |
| EVENT_BROADCAST(&threadpool.events.end); | |
| slurm_mutex_unlock(&threadpool.mutex); | |
| return NULL; | |
| } | |
| static void _free_attr(pthread_attr_t *attr) | |
| { | |
| int rc = EINVAL; | |
| if ((rc = pthread_attr_destroy(attr))) | |
| fatal("%s: pthread_attr_destroy failed: %s", | |
| __func__, slurm_strerror(rc)); | |
| } | |
| /* | |
| * Wait for thread assignment if the thread ID pointer needs to be populated. | |
| * Caller must hold threadpool.mutex lock. | |
| */ | |
| static void _assign_wait(thread_t *thread, pthread_t *id_ptr, | |
| const char *caller) | |
| { | |
| while (!thread->id) { | |
| xassert(thread->magic == THREAD_MAGIC); | |
| xassert((thread->state == THREAD_STATE_PENDING) || | |
| (thread->state == THREAD_STATE_CLONING)); | |
| xassert(pthread_equal(thread->requester, pthread_self())); | |
| log_flag(THREAD, "%s->%s: waiting for assignment for %s() with %d idle threads", | |
| caller, __func__, thread->func_name, threadpool.idle); | |
| EVENT_SIGNAL(&threadpool.events.assign); | |
| EVENT_WAIT(&threadpool.events.assigned, &threadpool.mutex); | |
| } | |
| xassert(thread->magic == THREAD_MAGIC); | |
| xassert(thread->state == THREAD_STATE_ASSIGNED_WAIT); | |
| xassert(pthread_equal(thread->requester, pthread_self())); | |
| xassert(thread->id); | |
| thread->requester = 0; | |
| *id_ptr = thread->id; | |
| thread->state = THREAD_STATE_ASSIGNED_ACK; | |
| log_flag(THREAD, "%s->%s: assigned pthread id=0x%"PRIx64" for %s()", | |
| caller, __func__, (uint64_t) thread->id, | |
| thread->func_name); | |
| EVENT_BROADCAST(&threadpool.events.assigned_ack); | |
| } | |
| static int _new_thread(thread_t *thread, pthread_t *id_ptr, const char *caller) | |
| { | |
| pthread_t id = 0; | |
| pthread_attr_t attr; | |
| int rc = EINVAL; | |
| const char *func_name = (thread ? thread->func_name : NULL); | |
| const bool detached = (thread && thread->detached); | |
| /* only threadpool will have pre-allocated threads */ | |
| xassert(thread || threadpool.enabled); | |
| if (thread) { | |
| xassert(thread->magic == THREAD_MAGIC); | |
| xassert(thread->state == THREAD_STATE_NEW); | |
| xassert(!thread->id); | |
| xassert(!thread->requester); | |
| thread->state = THREAD_STATE_CLONING; | |
| if (id_ptr) | |
| thread->requester = pthread_self(); | |
| } | |
| if ((rc = pthread_attr_init(&attr))) | |
| fatal("%s->%s: pthread_attr_init() failed: %s", | |
| caller, __func__, slurm_strerror(rc)); | |
| #ifdef PTHREAD_SCOPE_SYSTEM | |
| /* we want 1:1 threads if there is a choice */ | |
| if ((rc = pthread_attr_setscope(&attr, PTHREAD_SCOPE_SYSTEM))) | |
| fatal("%s->%s: pthread_attr_setscope(PTHREAD_SCOPE_SYSTEM) failed: %s", | |
| caller, __func__, slurm_strerror(rc)); | |
| #endif | |
| if ((rc = pthread_attr_setstacksize(&attr, STACK_SIZE))) | |
| fatal("%s->%s: pthread_attr_setstacksize(%u) failed: %s", | |
| caller, __func__, STACK_SIZE, slurm_strerror(rc)); | |
| /* All threadpool threads are always detached */ | |
| if ((threadpool.enabled || detached) && | |
| (rc = pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED))) | |
| fatal("%s->%s: pthread_attr_setdetachstate failed: %s", | |
| caller, __func__, slurm_strerror(rc)); | |
| /* Pass ownership of thread to new thread on success */ | |
| if ((rc = pthread_create(&id, &attr, | |
| (threadpool.enabled ? _threadpool_thread : | |
| _thread), | |
| thread))) | |
| fatal("%s->%s: pthread_create() failed: %s", | |
| caller, __func__, slurm_strerror(rc)); | |
| /* | |
| * Wait until thread->id is populated after the thread starts or | |
| * there is a race to the next call of join() or detach() | |
| */ | |
| if (id_ptr) { | |
| if (threadpool.enabled) { | |
| xassert(thread); | |
| slurm_mutex_lock(&threadpool.mutex); | |
| _assign_wait(thread, id_ptr, caller); | |
| xassert(pthread_equal(thread->id, id)); | |
| slurm_mutex_unlock(&threadpool.mutex); | |
| } else { | |
| xassert(id > 0); | |
| *id_ptr = id; | |
| } | |
| } | |
| xassert(threadpool.enabled || func_name); | |
| log_flag(THREAD, "%s->%s: pthread_create() created new %spthread id=0x%"PRIx64" for %s%s", | |
| caller, __func__, (threadpool.enabled ? "" : | |
| (detached ? "detached " : "attached ")), | |
| (uint64_t) id, (threadpool.enabled ? "threadpool" : func_name), | |
| (threadpool.enabled ? "" : "()")); | |
| _free_attr(&attr); | |
| return rc; | |
| } | |
| /* | |
| * True if there is at least 1 thread ready to run. | |
| * Caller must hold threadpool.mutex lock. | |
| */ | |
| static bool _thread_available(void) | |
| { | |
| const int pending = list_count(threadpool.pending); | |
| const int idle = threadpool.idle; | |
| const int zombies = threadpool.zombies; | |
| /* | |
| * The number of idle threads not stuck as zombies must be greater than | |
| * the current count of pending thread requested to have at least one | |
| * thread available to run. | |
| */ | |
| return ((idle - zombies) > pending); | |
| } | |
| static bool _assign(thread_t *thread, pthread_t *id_ptr, const char *caller) | |
| { | |
| slurm_mutex_lock(&threadpool.mutex); | |
| xassert(thread->magic == THREAD_MAGIC); | |
| xassert(!thread->requester); | |
| xassert(thread->state > THREAD_STATE_INVALID); | |
| xassert(thread->state < THREAD_STATE_INVALID_MAX); | |
| if (!_thread_available()) { | |
| log_flag(THREAD, "%s->%s: zero available idle threads for %s()", | |
| caller, __func__, thread->func_name); | |
| slurm_mutex_unlock(&threadpool.mutex); | |
| return false; | |
| } | |
| if (id_ptr) { | |
| /* | |
| * Only assign requester to have the thread signal upon | |
| * assignment but to skip waiting otherwise | |
| */ | |
| thread->requester = pthread_self(); | |
| } | |
| thread->state = THREAD_STATE_PENDING; | |
| list_append(threadpool.pending, thread); | |
| if (!id_ptr) { | |
| /* | |
| * No need to wait for assignment from pending list after waking | |
| * up an idle thread to accept the work | |
| */ | |
| EVENT_SIGNAL(&threadpool.events.assign); | |
| slurm_mutex_unlock(&threadpool.mutex); | |
| return true; | |
| } | |
| _assign_wait(thread, id_ptr, caller); | |
| /* thread should have removed ptr from pending list */ | |
| xassert(!list_delete_ptr(threadpool.pending, thread)); | |
| slurm_mutex_unlock(&threadpool.mutex); | |
| return true; | |
| } | |
| extern int threadpool_create(threadpool_func_t func, const char *func_name, | |
| void *arg, const bool detached, | |
| const char *thread_name, pthread_t *id_ptr, | |
| const char *caller) | |
| { | |
| thread_t *thread = xmalloc(sizeof(*thread)); | |
| if (id_ptr) | |
| *id_ptr = 0; | |
| #ifndef NDEBUG | |
| if (thread_name && strlen(thread_name) >= PRCTL_BUF_BYTES) | |
| warning("%s: Thread name truncated[%zu/%zu]: %s", | |
| caller, (uint64_t) strlen(thread_name), | |
| (uint64_t) PRCTL_BUF_BYTES, thread_name); | |
| #endif | |
| *thread = (thread_t) { | |
| .magic = THREAD_MAGIC, | |
| .state = THREAD_STATE_NEW, | |
| .detached = detached, | |
| .thread_name = xstrdup(thread_name), | |
| .func = func, | |
| .func_name = func_name, | |
| .arg = arg, | |
| .requested = timespec_now(), | |
| }; | |
| if (threadpool.enabled) { | |
| if (!detached) { | |
| slurm_mutex_lock(&threadpool.mutex); | |
| list_append(threadpool.attached, thread); | |
| slurm_mutex_unlock(&threadpool.mutex); | |
| } | |
| if (_assign(thread, id_ptr, caller)) | |
| return SLURM_SUCCESS; | |
| } | |
| return _new_thread(thread, id_ptr, caller); | |
| } | |
| static void _parse_params(const int default_count, const char *params) | |
| { | |
| char *tmp_str = NULL, *tok = NULL, *saveptr = NULL; | |
| if (default_count > 0) | |
| threadpool.config.preallocate = default_count; | |
| threadpool.enabled = true; | |
| if (!params) | |
| return; | |
| tmp_str = xstrdup(params); | |
| tok = strtok_r(tmp_str, ",", &saveptr); | |
| while (tok) { | |
| if (!xstrncasecmp(tok, THREADPOOL_PARAM, | |
| strlen(THREADPOOL_PARAM))) { | |
| const char *value = (tok + strlen(THREADPOOL_PARAM)); | |
| if (!xstrcasecmp(value, "enabled")) | |
| threadpool.enabled = true; | |
| else if (!xstrcasecmp(value, "disabled")) | |
| threadpool.enabled = false; | |
| else | |
| fatal("Invalid parameter %s", tok); | |
| log_flag(THREAD, "%s: threadpool is %s", | |
| __func__, | |
| (threadpool.enabled ? "enabled" : "disabled")); | |
| } else if ( | |
| !xstrncasecmp(tok, THREADPOOL_PARAM_PREALLOCATE, | |
| strlen(THREADPOOL_PARAM_PREALLOCATE))) { | |
| const unsigned long count = slurm_atoul( | |
| tok + strlen(THREADPOOL_PARAM_PREALLOCATE)); | |
| if (count > THREADPOOL_MAX_THREADS) | |
| fatal("%s: invalid parameter %s", | |
| __func__, tok); | |
| threadpool.config.preallocate = count; | |
| log_flag(THREAD, "%s: preallocate %lu threads", | |
| __func__, count); | |
| } else if (!xstrncasecmp(tok, THREADPOOL_PARAM_PRESERVE, | |
| strlen(THREADPOOL_PARAM_PRESERVE))) { | |
| const unsigned long count = | |
| slurm_atoul(tok + | |
| strlen(THREADPOOL_PARAM_PRESERVE)); | |
| if (count > THREADPOOL_MAX_THREADS) | |
| fatal("%s: invalid parameter %s", | |
| __func__, tok); | |
| threadpool.config.preserve = count; | |
| log_flag(THREAD, "%s: preserve %lu threads", | |
| __func__, count); | |
| } else { | |
| log_flag(THREAD, "%s: threadpool ignoring parameter %s", | |
| __func__, tok); | |
| } | |
| tok = strtok_r(NULL, ",", &saveptr); | |
| } | |
| xfree(tmp_str); | |
| } | |
| static int _log_thread(void *x, void *arg) | |
| { | |
| log_thread_t *logt = arg; | |
| thread_t *thread = x; | |
| xassert(logt->magic == LOG_THREAD_MAGIC); | |
| xassert(thread->magic == THREAD_MAGIC); | |
| probe_log(logt->log, "thread[%s@0x%"PRIx64"]: func=%s(0x%"PRIxPTR") type=%s state=%s detached=%c requester=0x%"PRIx64" ret=0x%"PRIxPTR, | |
| thread->thread_name, (uint64_t) thread->id, | |
| thread->func_name, (uintptr_t) thread->arg, logt->type, | |
| _thread_state_str(thread->state), | |
| BOOL_CHARIFY(thread->detached), (uint64_t) thread->requester, | |
| (uintptr_t) thread->ret); | |
| return 0; | |
| } | |
| /* Caller must hold threadpool.mutex lock */ | |
| static void _probe_verbose(probe_log_t *log) | |
| { | |
| log_thread_t logt = { | |
| .magic = LOG_THREAD_MAGIC, | |
| .log = log, | |
| }; | |
| char histogram[LATENCY_METRIC_HISTOGRAM_STR_LEN] = { 0 }; | |
| probe_log(log, "config: preallocate:%d preserve:%d", | |
| threadpool.config.preallocate, threadpool.config.preserve); | |
| probe_log( | |
| log, | |
| "state: shutdown:%c pending:%d zombies:%d attached:%d running:%d idle:%d total_run:%" PRIu64 " total_created:%" PRIu64 " peak_count:%" PRIu64, | |
| BOOL_CHARIFY(threadpool.shutdown), | |
| list_count(threadpool.pending), threadpool.zombies, | |
| list_count(threadpool.attached), threadpool.running, | |
| threadpool.idle, threadpool.total_run, threadpool.total_created, | |
| threadpool.peak_count); | |
| logt.type = "pending"; | |
| (void) list_for_each_ro(threadpool.pending, _log_thread, &logt); | |
| logt.type = "attached"; | |
| (void) list_for_each_ro(threadpool.attached, _log_thread, &logt); | |
| (void) latency_histogram_print_labels(histogram, sizeof(histogram)); | |
| probe_log(log, "histogram: %s", histogram); | |
| (void) latency_histogram_print(&threadpool.histograms.request, | |
| histogram, sizeof(histogram)); | |
| probe_log(log, "request histogram: %s", histogram); | |
| (void) latency_histogram_print(&threadpool.histograms.run, histogram, | |
| sizeof(histogram)); | |
| probe_log(log, "run histogram: %s", histogram); | |
| (void) latency_histogram_print(&threadpool.histograms.join, histogram, | |
| sizeof(histogram)); | |
| probe_log(log, "join histogram: %s", histogram); | |
| } | |
| static probe_status_t _probe(probe_log_t *log, void *arg) | |
| { | |
| probe_status_t status = PROBE_RC_UNKNOWN; | |
| slurm_mutex_lock(&threadpool.mutex); | |
| if (log) | |
| _probe_verbose(log); | |
| if (!threadpool.enabled) | |
| status = PROBE_RC_UNKNOWN; | |
| else if (threadpool.shutdown) | |
| status = PROBE_RC_ONLINE; | |
| else if ((threadpool.running + threadpool.idle) > | |
| THREADPOOL_MAX_THREADS) | |
| status = PROBE_RC_BUSY; | |
| else | |
| status = PROBE_RC_READY; | |
| slurm_mutex_unlock(&threadpool.mutex); | |
| return status; | |
| } | |
| extern void threadpool_init(const int default_count, const char *params) | |
| { | |
| int preallocate = -1; | |
| _parse_params(default_count, params); | |
| slurm_mutex_lock(&threadpool.mutex); | |
| if (threadpool.pending || threadpool.shutdown || !threadpool.enabled) { | |
| slurm_mutex_unlock(&threadpool.mutex); | |
| return; | |
| } | |
| xassert(!threadpool.shutdown); | |
| xassert(threadpool.enabled); | |
| xassert(!threadpool.pending); | |
| threadpool.pending = list_create(NULL); | |
| xassert(!threadpool.attached); | |
| threadpool.attached = list_create(NULL); | |
| preallocate = threadpool.config.preallocate; | |
| slurm_mutex_unlock(&threadpool.mutex); | |
| probe_register("threadpool", _probe, NULL); | |
| xassert(preallocate >= 0); | |
| for (int i = 0; i < preallocate; i++) | |
| _new_thread(NULL, NULL, __func__); | |
| log_flag(THREAD, "%s: started with %d threads preallocated", | |
| __func__, preallocate); | |
| } | |
| extern void threadpool_fini(void) | |
| { | |
| slurm_mutex_lock(&threadpool.mutex); | |
| if (!threadpool.enabled) { | |
| slurm_mutex_unlock(&threadpool.mutex); | |
| return; | |
| } | |
| /* | |
| * Never change threadpool.enabled to false to avoid race conditions of | |
| * checking if threadpool was ever enabled | |
| */ | |
| threadpool.shutdown = true; | |
| /* Wake every idle worker to clean up */ | |
| EVENT_BROADCAST(&threadpool.events.assign); | |
| /* | |
| * Do not wait for all workers to exit before returning. Most callers | |
| * invoke threadpool_fini() as a shutdown trigger rather than a | |
| * synchronous join, and waiting here would wait forever: workers in | |
| * ZOMBIE are blocked on events.detach (only broadcast by | |
| * _threadpool_on_detach() via join/detach, which fini does not issue), | |
| * workers in ASSIGNED_WAIT are blocked on events.assigned_ack from a | |
| * requester that may never return, and workers in RUNNING are inside | |
| * arbitrary user func() that may itself block on I/O or other Slurm | |
| * subsystems that fini's caller is about to tear down. Workers that | |
| * are reachable will observe threadpool.shutdown on their next trip | |
| * through _threadpool_thread() and exit on their own. | |
| */ | |
| slurm_mutex_unlock(&threadpool.mutex); | |
| } | |
| static int _threadpool_detach(const pthread_t id, const char *caller) | |
| { | |
| thread_t *thread = NULL; | |
| int rc = EINVAL; | |
| slurm_mutex_lock(&threadpool.mutex); | |
| if (!(thread = list_find_first(threadpool.attached, _match_thread_id, | |
| (void *) &id))) { | |
| log_flag(THREAD, "%s->%s: pthread id=0x%"PRIx64" not found", | |
| caller, __func__, (uint64_t) id); | |
| rc = ESRCH; | |
| } else { | |
| rc = _threadpool_on_detach(thread, caller); | |
| } | |
| slurm_mutex_unlock(&threadpool.mutex); | |
| return rc; | |
| } | |
| static int _detach(const pthread_t id, const char *caller) | |
| { | |
| int rc = EINVAL; | |
| if ((rc = pthread_detach(id))) | |
| error("%s->%s: pthread_detach(id=0x%"PRIx64") failed: %s", | |
| caller, __func__, (uint64_t) id, slurm_strerror(rc)); | |
| return rc; | |
| } | |
| extern int threadpool_detach(const pthread_t id, const char *caller) | |
| { | |
| if (!id) { | |
| log_flag(THREAD, "%s->%s: Ignoring invalid pthread id=0x0", | |
| caller, __func__); | |
| return SLURM_SUCCESS; | |
| } | |
| if (threadpool.enabled) | |
| return _threadpool_detach(id, caller); | |
| else | |
| return _detach(id, caller); | |
| } |