[PROPOSED 1/2] Optionally use read-write locks instead of mutexes
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
* Makefile, NEWS: Mention this. * localtime.c (THREAD_PREFER_SINGLE): Default to 0. (HAVE___ISTHREADED, HAVE_SYS_SINGLE_THREADED_H): Provide defaults. (dolock, dounlock): New functions, whose bodies are those of the old lock and unlock functions. (lock): Return -1 if known to be single threaded so no lock is needed. All callers changed. (unlock, rd2wrlock, tzset_unlocked): New arg THREADED. All callers changed. --- Makefile | 9 +++- NEWS | 7 +++ localtime.c | 125 ++++++++++++++++++++++++++++++++++++++-------------- 3 files changed, 106 insertions(+), 35 deletions(-) diff --git a/Makefile b/Makefile index c2c82c3a..8939869a 100644 --- a/Makefile +++ b/Makefile @@ -304,7 +304,14 @@ 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: +# The following options can also be used: +# -DTHREAD_PREFER_SINGLE to prefer speed in single-threaded apps, +# at some cost in CPU time and energy in multi-threaded apps. +# The following options can also be used: +# -DHAVE___ISTHREADED=1 if there is an extern int __isthreaded +# variable, 0 otherwise (default is guessed) +# -DHAVE_SYS_SINGLE_THREADED_H=0 if <sys/single_threaded.h> works, +# 0 otherwise (default is guessed) # -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. diff --git a/NEWS b/NEWS index 8ba97ab4..5bfb7bb4 100644 --- a/NEWS +++ b/NEWS @@ -68,6 +68,13 @@ 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_PREFER_SINGLE causes tzcode + in single-threaded processes to avoid locks, as FreeBSD does. + This can save time in single-threaded apps. The threadedness + testing costs CPU time and energy in multi-threaded apps. + New options -DHAVE___ISTHREADED and -DHAVE_SYS_SINGLE_THREADED_H + can help configure how to test for single-threadedness. + 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. diff --git a/localtime.c b/localtime.c index 3a38ef04..01db7e70 100644 --- a/localtime.c +++ b/localtime.c @@ -50,31 +50,87 @@ struct stat { char st_ctime, st_dev, st_ino; } #if defined THREAD_SAFE && THREAD_SAFE # include <pthread.h> + +# ifndef THREAD_PREFER_SINGLE +# define THREAD_PREFER_SINGLE 0 +# endif +# if THREAD_PREFER_SINGLE +# ifndef HAVE___ISTHREADED +# if defined __FreeBSD__ || defined __OpenBSD__ +# define HAVE___ISTHREADED 1 +# else +# define HAVE___ISTHREADED 0 +# endif +# endif +# if HAVE___ISTHREADED +extern int __isthreaded; +# else +# if !defined HAVE_SYS_SINGLE_THREADED_H && defined __has_include +# if __has_include(<sys/single_threaded.h>) +# define HAVE_SYS_SINGLE_THREADED_H 1 +# else +# define HAVE_SYS_SINGLE_THREADED_H 0 +# endif +# endif +# ifndef HAVE_SYS_SINGLE_THREADED_H +# if defined __GLIBC__ && 2 < __GLIBC__ + (32 <= __GLIBC_MINOR__) +# define HAVE_SYS_SINGLE_THREADED_H 1 +# else +# define HAVE_SYS_SINGLE_THREADED_H 0 +# endif +# endif +# if HAVE_SYS_SINGLE_THREADED_H +# include <sys/single_threaded.h> +# endif +# endif +# endif + # 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); } +static int dolock(void) { return pthread_rwlock_rdlock(&locallock); } +static void dounlock(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); } +static int dolock(void) { return pthread_mutex_lock(&locallock); } +static void dounlock(void) { pthread_mutex_unlock(&locallock); } # endif +/* Get a lock. Return 0 on success, a positive errno value on failure, + negative if known to be single-threaded so no lock is needed. */ +static int +lock(void) +{ +# if THREAD_PREFER_SINGLE && HAVE___ISTHREADED + if (!__isthreaded) + return -1; +# elif THREAD_PREFER_SINGLE && HAVE_SYS_SINGLE_THREADED_H + if (__libc_single_threaded) + return -1; +# endif + return dolock(); +} +static void +unlock(bool threaded) +{ + if (threaded) + dounlock(); +} #else -static int lock(void) { return 0; } -static void unlock(void) { } +static int lock(void) { return -1; } +static void unlock(ATTRIBUTE_MAYBE_UNUSED bool threaded) { } #endif -/* Upgrade a read lock to a write lock. - Return 0 on success, an errno value otherwise. */ +/* If THREADED, upgrade a read lock to a write lock. + Return 0 on success, a positive errno value otherwise. */ static int -rd2wrlock(void) +rd2wrlock(ATTRIBUTE_MAYBE_UNUSED bool threaded) { # if THREAD_RWLOCK - unlock(); - return pthread_rwlock_wrlock(&locallock); -# else - return 0; + if (threaded) { + dounlock(); + return pthread_rwlock_wrlock(&locallock); + } # endif + return 0; } /* Unless intptr_t is missing, pacify gcc -Wcast-qual on char const * exprs. @@ -1756,11 +1812,11 @@ 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, + If THREADED && 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) +tzset_unlocked(bool threaded, monotime_t now) { char const *name; struct state *sp; @@ -1800,7 +1856,7 @@ tzset_unlocked(monotime_t now) if (!THREAD_RWLOCK || writing) break; - if (rd2wrlock() != 0) + if (rd2wrlock(threaded) != 0) return; writing = true; } @@ -1853,12 +1909,12 @@ tzset(void) { monotime_t now = get_monotonic_time(); int err = lock(); - if (err != 0) { + if (0 < err) { errno = err; return; } - tzset_unlocked(now); - unlock(); + tzset_unlocked(!err, now); + unlock(!err); } #endif @@ -1866,10 +1922,11 @@ static void gmtcheck(void) { static bool gmt_is_set; - if (lock() != 0) + int err = lock(); + if (0 < err) return; if (! gmt_is_set) { - if (rd2wrlock() != 0) + if (rd2wrlock(!err) != 0) return; if (!THREAD_RWLOCK || !gmt_is_set) { #if ALL_STATE @@ -1880,7 +1937,7 @@ gmtcheck(void) gmt_is_set = true; } } - unlock(); + unlock(!err); } #if NETBSD_INSPIRED && !USE_TIMEX_T @@ -2047,14 +2104,14 @@ localtime_tzset(time_t const *timep, struct tm *tmp, bool setname) { monotime_t now = get_monotonic_time(); int err = lock(); - if (err) { + if (0 < err) { errno = err; return NULL; } if (0 <= tz_change_interval || setname || !lcl_is_set) - tzset_unlocked(now); + tzset_unlocked(!err, now); tmp = localsub(lclptr, timep, setname, tmp); - unlock(); + unlock(!err); return tmp; } @@ -2715,13 +2772,13 @@ mktime(struct tm *tmp) monotime_t now = get_monotonic_time(); time_t t; int err = lock(); - if (err) { + if (0 < err) { errno = err; return -1; } - tzset_unlocked(now); + tzset_unlocked(!err, now); t = mktime_tzname(lclptr, tmp, true); - unlock(); + unlock(!err); return t; } @@ -2832,15 +2889,15 @@ time2posix(time_t t) { monotime_t now = get_monotonic_time(); int err = lock(); - if (err) { + if (0 < err) { errno = err; return -1; } if (0 <= tz_change_interval || !lcl_is_set) - tzset_unlocked(now); + tzset_unlocked(!err, now); if (lclptr) t = time2posix_z(lclptr, t); - unlock(); + unlock(!err); return t; } @@ -2878,15 +2935,15 @@ posix2time(time_t t) { monotime_t now = get_monotonic_time(); int err = lock(); - if (err) { + if (0 < err) { errno = err; return -1; } if (0 <= tz_change_interval || !lcl_is_set) - tzset_unlocked(now); + tzset_unlocked(!err, now); if (lclptr) t = posix2time_z(lclptr, t); - unlock(); + unlock(!err); return t; } -- 2.51.0
participants (1)
-
Paul Eggert