This avoids bottlenecks when many threads access local time but no thread is changing TZ. It costs CPU time and energy. * Makefile, NEWS: Mention this. * localtime.c (THREAD_RWLOCK): New macro, defaulting to 0. (locallock) [THREAD_RWLOCK]: Now of type pthread_rwlock_t, not pthread_mutex_t. All uses changed. (rd2wrlock): New function. (tzset_unlocked): If THREAD_RWLOCK, upgrade the read lock to a write lock if needed. (tzset_unlocked, gmtcheck): Upgrade the read to a write lock in the unlikely case where writing is needed. --- Makefile | 5 +++ NEWS | 5 +++ localtime.c | 100 +++++++++++++++++++++++++++++++++++++--------------- 3 files changed, 82 insertions(+), 28 deletions(-) diff --git a/Makefile b/Makefile index 85c2c50b..c2c82c3a 100644 --- a/Makefile +++ b/Makefile @@ -304,6 +304,11 @@ LDLIBS= # -DTHREAD_SAFE to make localtime.c thread-safe, as POSIX requires; # not needed by the main-program tz code, which is single-threaded. # Append other compiler flags as needed, e.g., -pthread on GNU/Linux. +# The following option can also be used: +# -DTHREAD_RWLOCK to use read-write locks intead of mutexes. +# This can improve paralellism and thus save real time +# if many threads call tzcode functions simultaneously. +# It also costs CPU time and thus energy. # -Dtime_tz=\"T\" to use T as the time_t type, rather than the system time_t # This is intended for internal use only; it mangles external names. # -DTZ_CHANGE_INTERVAL=N if functions depending on TZ should check diff --git a/NEWS b/NEWS index 64d9336d..8ba97ab4 100644 --- a/NEWS +++ b/NEWS @@ -68,6 +68,11 @@ Unreleased, experimental changes transition begun in release 96k, which removed spaces in tzdata because the spaces break time string parsers. + The new CFLAGS option -DTHREAD_RWLOCK uses read-write locks, as + macOS does, instead of mutexes. This saves real time when TZ is + rarely changing and many threads call tzcode simultaneously. + It costs more CPU time and energy. + tzcode now uses mempcpy if available, guessing its availability. Compile with -DHAVE_MEMPCPY=1 or 0 to override the guess. diff --git a/localtime.c b/localtime.c index ec13ed71..3a38ef04 100644 --- a/localtime.c +++ b/localtime.c @@ -44,16 +44,39 @@ struct stat { char st_ctime, st_dev, st_ino; } # define st_ctim st_ctimespec #endif +#ifndef THREAD_RWLOCK +# define THREAD_RWLOCK 0 +#endif + #if defined THREAD_SAFE && THREAD_SAFE # include <pthread.h> +# if THREAD_RWLOCK +static pthread_rwlock_t locallock = PTHREAD_RWLOCK_INITIALIZER; +static int lock(void) { return pthread_rwlock_rdlock(&locallock); } +static void unlock(void) { pthread_rwlock_unlock(&locallock); } +# else static pthread_mutex_t locallock = PTHREAD_MUTEX_INITIALIZER; static int lock(void) { return pthread_mutex_lock(&locallock); } static void unlock(void) { pthread_mutex_unlock(&locallock); } +# endif #else static int lock(void) { return 0; } static void unlock(void) { } #endif +/* Upgrade a read lock to a write lock. + Return 0 on success, an errno value otherwise. */ +static int +rd2wrlock(void) +{ +# if THREAD_RWLOCK + unlock(); + return pthread_rwlock_wrlock(&locallock); +# else + return 0; +# endif +} + /* Unless intptr_t is missing, pacify gcc -Wcast-qual on char const * exprs. Use this carefully, as the casts disable type checking. This is a macro so that it can be used in static initializers. */ @@ -1733,38 +1756,55 @@ zoneinit(struct state *sp, char const *name, char tzloadflags) } /* Like tzset(), but in a critical section. + If THREAD_RWLOCK the caller has a read lock, + and this function might ugrade it to a write lock. If tz_change_interval is positive the time is NOW; otherwise ignore NOW. */ static void tzset_unlocked(monotime_t now) { - char const *name = getenv("TZ"); - struct state *sp = lclptr; - char tzloadflags = TZLOAD_FROMENV | TZLOAD_TZSTRING; - size_t namelen = sizeof lcl_TZname + 1; /* placeholder for no name */ - - if (name) { - namelen = strnlen(name, sizeof lcl_TZname); - - /* Abbreviate a string like "/usr/share/zoneinfo/America/Los_Angeles" - to its shorter equivalent "America/Los_Angeles". */ - if (!SUPPRESS_TZDIR && tzdirslashlen < namelen - && memcmp(name, tzdirslash, tzdirslashlen) == 0) { - char const *p = name + tzdirslashlen; - while (*p == '/') - p++; - if (*p && *p != ':') { - name = p; - namelen = strnlen(name, sizeof lcl_TZname); - tzloadflags |= TZLOAD_TZDIR_SUB; + char const *name; + struct state *sp; + char tzloadflags; + size_t namelen; + bool writing = false; + + for (;;) { + name = getenv("TZ"); + sp = lclptr; + tzloadflags = TZLOAD_FROMENV | TZLOAD_TZSTRING; + namelen = sizeof lcl_TZname + 1; /* placeholder for no name */ + + if (name) { + namelen = strnlen(name, sizeof lcl_TZname); + + /* Abbreviate a string like "/usr/share/zoneinfo/America/Los_Angeles" + to its shorter equivalent "America/Los_Angeles". */ + if (!SUPPRESS_TZDIR && tzdirslashlen < namelen + && memcmp(name, tzdirslash, tzdirslashlen) == 0) { + char const *p = name + tzdirslashlen; + while (*p == '/') + p++; + if (*p && *p != ':') { + name = p; + namelen = strnlen(name, sizeof lcl_TZname); + tzloadflags |= TZLOAD_TZDIR_SUB; + } } } + + if ((tz_change_interval <= 0 ? tz_change_interval < 0 : fresh_tzdata(now)) + && (name + ? 0 < lcl_is_set && strcmp(lcl_TZname, name) == 0 + : lcl_is_set < 0)) + return; + + if (!THREAD_RWLOCK || writing) + break; + if (rd2wrlock() != 0) + return; + writing = true; } - if ((tz_change_interval <= 0 ? tz_change_interval < 0 : fresh_tzdata(now)) - && (name - ? 0 < lcl_is_set && strcmp(lcl_TZname, name) == 0 - : lcl_is_set < 0)) - return; # if ALL_STATE if (! sp) lclptr = sp = malloc(sizeof *lclptr); @@ -1829,12 +1869,16 @@ gmtcheck(void) if (lock() != 0) return; if (! gmt_is_set) { + if (rd2wrlock() != 0) + return; + if (!THREAD_RWLOCK || !gmt_is_set) { #if ALL_STATE - gmtptr = malloc(sizeof *gmtptr); + gmtptr = malloc(sizeof *gmtptr); #endif - if (gmtptr) - gmtload(gmtptr); - gmt_is_set = true; + if (gmtptr) + gmtload(gmtptr); + gmt_is_set = true; + } } unlock(); } -- 2.51.0