Added for fx (module fx-sync v0.1.1) sync primitives & (currently only unbounded-push impl.) queue, with MT signal usage handles.
Initial commit Ported test function from `otp-simple`. All works okay! Fortune for libfx-sync's current commit: Curse − 凶master
commit
fbbf7e5ea9
@ -0,0 +1,15 @@
|
||||
*.o
|
||||
*.a
|
||||
*.gch
|
||||
*.so
|
||||
*.so.*
|
||||
*.a
|
||||
|
||||
*-release
|
||||
*-debug
|
||||
*-pgo
|
||||
*-test
|
||||
|
||||
old/
|
||||
obj/
|
||||
prof/
|
@ -0,0 +1,471 @@
|
||||
# Mostly-header library for threaded-programming sync primitives & helpers `fx::[shared::]{Mutex,CondVar}`, `fx::Queue<T>`, & namespace `fx::signal`.
|
||||
#
|
||||
# Likely prefer static-linking. Non-header elements are required for `fx::error` & certain required features of `fx::signal::`
|
||||
#
|
||||
# Contains targets for `release', `debug', and `clean'.
|
||||
PROJECT=fx-sync
|
||||
AUTHOR=Avril (Flanchan) <flanchan@cumallover.me>
|
||||
DESCRIPTION=sync module of libfx
|
||||
VERSION=0.1.1
|
||||
|
||||
SRC = src
|
||||
|
||||
SRC_C = $(shell find $(SRC)/ -type f -name \*.c -and -not -path $(SRC)/test/\* )
|
||||
SRC_CXX = $(shell find -O2 $(SRC)/ -type f \( -name \*.cpp -or -name \*.cxx \) -and -not -path $(SRC)/test/\* )
|
||||
|
||||
INCLUDE=include
|
||||
# If PCH should be auto-included for all TUs, set to 1.
|
||||
INCLUDE_PCH_GLOBAL?=0
|
||||
# Files to be auto-included by all TUs (after PCH global, if `INCLUDE_PCH_GLOBAL == 1`.)
|
||||
INCLUDE_GLOBAL=
|
||||
|
||||
# Link to these libraries dynamicalls
|
||||
SHARED_LIBS=
|
||||
# Link to these libraries statically
|
||||
STATIC_LIBS=
|
||||
|
||||
# Prefix for un/install targets, if it's blank, ensure it is set (default to `/usr/local` instead of `/usr`.)
|
||||
ifeq ($(PREFIX),)
|
||||
PREFIX := /usr/local
|
||||
endif
|
||||
|
||||
# Prefix for un/install target headers (default: all of `$(INCLUDE)`)
|
||||
# Make blank for default install to PREFIX/include
|
||||
# TODO: Do we want to turn this to `fx/sync/` instead of `fx-sync/`? What if `fx` is taken in usr/include..? (XXX: Or if other fx-* projects have things of the same name (esp. error.hh which is currently not standardised...?) Leave it as fx-sync for now i think...)
|
||||
INCLUDE_PREFIX := $(PROJECT)/
|
||||
|
||||
# Default archivers
|
||||
AR?=ar
|
||||
RANLIB?=ranlib
|
||||
|
||||
# Default linker
|
||||
LINKER?=gold
|
||||
|
||||
# Use gcc-{ar,ranlib} when using gcc
|
||||
ifeq ($(CXX),g++)
|
||||
AR:=gcc-ar
|
||||
RANLIB:=gcc-ranlib
|
||||
endif
|
||||
|
||||
# Pre-compile these headers
|
||||
PCH_HEADERS+=
|
||||
# PCH_HEADERS depend on these header files
|
||||
PCH_INCLUDES+=
|
||||
|
||||
#TODO: Add in the configurable opt levels from `work/map-rc/Makefile`
|
||||
|
||||
# Compile-time default program features (see `Features application` below.)
|
||||
FEATURES?=
|
||||
|
||||
# Build constants
|
||||
CONSTANTS+=_GNU_SOURCE __API_EXPORT__=__attribute__\(\(\visibility\(\"default\"\)\)\)
|
||||
|
||||
# Default visibility ("default" / "", "hidden", "protected", "internal".)
|
||||
# If not exported, symbols must be manually exported.
|
||||
SYMBOL_VISIBILITY?=default
|
||||
|
||||
# Reduce binary size by removing inline address identity & define symbols relevant only to exported symbols
|
||||
#
|
||||
# NOTE: This requires either `SYMBOL_VISIBILITY=hidden` or manual non-exported symbol hiding to be effective.
|
||||
# Only disable this if `inline`s need address identity.
|
||||
STRICT_EXPORT?=yes
|
||||
|
||||
# Testing
|
||||
# Can be {debug,release}.{a,so}
|
||||
TARGET?=debug.a
|
||||
|
||||
TEST_LDFLAGS+= -lfmt -lstdc++ -Wl,-z -Wl,now -Wl,-z -Wl,relro
|
||||
TEST_CFLAGS+= -Og -g -fwhole-program
|
||||
|
||||
override __COMMA=,
|
||||
override __VERSION_SPLIT:= $(subst ., ,$(VERSION))
|
||||
override __VERSION_REVISION:=$(word 3,$(__VERSION_SPLIT)) 0
|
||||
|
||||
VERSION_MAJOR:= $(word 1,$(__VERSION_SPLIT))
|
||||
VERSION_MINOR:= $(word 2,$(__VERSION_SPLIT))
|
||||
VERSION_BUGFIX:= $(word 3,$(__VERSION_SPLIT))
|
||||
VERSION_REVISION:= $(word 2,$(subst r, ,$(__VERSION_REVISION)))
|
||||
|
||||
override __VERSION_SPLIT:= MAJOR:$(word 1,$(__VERSION_SPLIT)) MINOR:$(word 2,$(__VERSION_SPLIT)) BUGFIX:$(word 1,$(subst r, ,$(__VERSION_REVISION))) REVISION:$(word 2,$(subst r, ,$(__VERSION_REVISION))) REVISION_STRING:$(word 3,$(__VERSION_SPLIT))
|
||||
|
||||
COMMON_FLAGS+= -W -Wall
|
||||
COMMON_FLAGS+= $(addprefix -D,$(CONSTANTS))
|
||||
COMMON_FLAGS+= -pipe -Wstrict-aliasing -fno-strict-aliasing $(addprefix -I,$(INCLUDE))
|
||||
COMMON_FLAGS+= $(addprefix -D_VERSION_,$(subst :,=,$(__VERSION_SPLIT))) '-D_PROJECT_VERSION="$(VERSION)"' '-D_PROJECT_AUTHOR="$(AUTHOR)"' '-D_PROJECT_NAME="$(PROJECT)"'
|
||||
COMMON_FLAGS+= $(addprefix -fvisibility=,$(SYMBOL_VISIBILITY))
|
||||
ifeq ($(STRICT_EXPORT),yes)
|
||||
COMMON_FLAGS+= -fvisibility-inlines-hidden -ffunction-sections -fdata-sections
|
||||
endif
|
||||
|
||||
# PCH targets for `%_p.hh` -> `%_p.hh.gch`, add `-include $(PCH_HEADERS)` to general `COMMON_FLAGS`, and add $(PCH_OUT) as a requirement for all `%.c/pp` targets as well. (where `PCH_OUT= $(addsuffix .gch,$(PCH_HEADERS))`.)
|
||||
|
||||
# Target arch & CPU. Set to blank for generic
|
||||
ARCH?=native
|
||||
CPU?=native
|
||||
# Enable OpenMP and loop parallelisation? (dyn-links to openmp, allows C++STL algorithms to use parallel execution mode when provided, enables auto-parallelise optimisation of loops in `release` build only.)
|
||||
#
|
||||
# To enable in CXX without auto-optimisation, set to `maybe` instead of `yes`. (__NOTE__: When set to `maybe`, if C++ parallel execution modes are used (from the `<execution>` header,) you will need to manually add `tbb` to `SHARED_LIBS`. `yes` and `force` add `-ltbb` automatically alongside `-fopenmp`.)
|
||||
# To enable throughout, *regardless of* release-only optimisations, set to `force`, (`yes` only enables to always in CXX, and only enables it for CC in release builds.)
|
||||
# To *only ever* enable in release builds, *regardless of optimisations*, set to `release` (which acts as `yes` but only in release builds.)
|
||||
PARALLEL?=no
|
||||
|
||||
# Set exception mode to:
|
||||
# - Empty - Exceptions are left as the default for CXX, and nothing about them is communicated to CC either.
|
||||
# - 'debug' (default) - Same as above, but `-fexceptions` is passed to CC in debug builds only. (XXX: Is this useful, to not pass it to CXX as well in debug...?)
|
||||
# - 'yes' - Pass `-fexceptions` to CC.
|
||||
# - 'no' - Pass `-fno-exceptions` to CXX.
|
||||
# - 'force' - Pass `-fexceptions` to CC and CXX always.
|
||||
# - 'never' - Pass `-fno-exceptions` to CC and CXX always.
|
||||
EXCEPTIONS?=force
|
||||
# Enable CPU-specific features
|
||||
CPU_FLAGS?=
|
||||
|
||||
BINFLAGS+=
|
||||
DEBUG_BINFLAGS+=
|
||||
RELEASE_BINFLAGS+= -fuse-linker-plugin
|
||||
|
||||
OPT_FLAGS?= -fgraphite \
|
||||
-floop-interchange -ftree-loop-distribution -floop-strip-mine -floop-block \
|
||||
-fno-stack-check
|
||||
|
||||
# Static and shared common flags
|
||||
SHARED_FLAGS+=-fPIC
|
||||
SHARED_RELEASE_FLAGS+=
|
||||
SHARED_DEBUG_FLAGS+=
|
||||
|
||||
STATIC_FLAGS+=
|
||||
STATIC_RELEASE_FLAGS+=-ffat-lto-objects
|
||||
STATIC_DEBUG_FLAGS+=
|
||||
|
||||
# Features application
|
||||
|
||||
## Tell program which features are enabled via `FEATURE_<feature name in UPPER_SNAKE_CASE>`.
|
||||
override __FEATURES=$(shell echo "$(FEATURES)" | tr '[:lower:]' '[:upper:]' | tr '-' '_')
|
||||
COMMON_FLAGS+=$(addsuffix =1,$(addprefix -DFEATURE_,$(__FEATURES)))
|
||||
|
||||
## Specific feature additions
|
||||
|
||||
# ## Example:
|
||||
#ifneq (,$(findstring fast-math,$(FEATURES)))
|
||||
# # fast-math feature: Apply `-ffast-math`
|
||||
# COMMON_FLAGS+=-ffast-math
|
||||
#endif
|
||||
|
||||
# Add linker
|
||||
ifneq ($(LINKER),)
|
||||
COMMON_FLAGS+=-fuse-ld=$(LINKER)
|
||||
endif
|
||||
|
||||
# Arch and optimisation
|
||||
ifneq ($(ARCH),)
|
||||
OPT_FLAGS+= $(addprefix -march=,$(ARCH))
|
||||
endif
|
||||
|
||||
ifneq ($(CPU),)
|
||||
OPT_FLAGS+= $(addprefix -mtune=,$(CPU))
|
||||
endif
|
||||
|
||||
# Add parallelisation flags
|
||||
override __PARALLEL_OPT_FLAGS=-floop-parallelize-all -ftree-parallelize-loops=4
|
||||
|
||||
ifeq ($(PARALLEL),yes)
|
||||
# NOTE: `-fopenmp` adds parallelised STL algorithms by default, so we want C++ (debug & release) TUs to always be compiled with it
|
||||
# XXX: Should we add this in for all `COMMON_FLAGS` to enable to C TUs too? Will this make the interop / debugging any better to *only* enable it in C for release builds??
|
||||
|
||||
COMMON_FLAGS+= -fopenmp -DPARALLEL=1
|
||||
OPT_FLAGS+= $(__PARALLEL_OPT_FLAGS)
|
||||
|
||||
# For C++ `std::execution` parallel execution models.
|
||||
LDFLAGS+= -ltbb -lgomp
|
||||
endif
|
||||
ifeq ($(PARALLEL),maybe)
|
||||
# Define `PARALLEL` constant, but to 0. So `#ifdef` and `#if` can distinguish.
|
||||
CFLAGS+= -DPARALLEL=0
|
||||
# Add openmp to C & CXX, without adding loop auto-parallelise optimisation
|
||||
CXXFLAGS+= -fopenmp -DPARALLEL=1 -DPARALLEL_CXX
|
||||
endif
|
||||
ifeq ($(PARALLEL),force)
|
||||
# Add openmp to C & CXX
|
||||
COMMON_FLAGS+= -fopenmp -DPARALLEL=1 -DPARALLEL_FORCE
|
||||
OPT_FLAGS+= $(__PARALLEL_OPT_FLAGS)
|
||||
|
||||
# For C++ `std::execution` parallel execution models.
|
||||
LDFLAGS+= -ltbb -lgomp
|
||||
endif
|
||||
ifeq ($(PARALLEL),release)
|
||||
# Enable OMP & parallel opt in `release` mode only
|
||||
|
||||
# NOTE: Adds in non-opt flags *after* target-spec below.
|
||||
OPT_FLAGS+= $(__PARALLEL_OPT_FLAGS)
|
||||
endif
|
||||
|
||||
COMMON_FLAGS+=$(addprefix -m,$(CPU_FLAGS))
|
||||
|
||||
CXX_OPT_FLAGS?= $(OPT_FLAGS) -felide-constructors
|
||||
|
||||
COMMON_STD=23
|
||||
|
||||
CSTD?=gnu$(COMMON_STD)
|
||||
CXXSTD?=gnu++$(COMMON_STD)
|
||||
|
||||
# Build Options
|
||||
|
||||
STRIP=strip
|
||||
|
||||
# XXX: Is `-fno-plt` actually helpful here?
|
||||
RELEASE_COMMON_FLAGS+= -fno-plt -fno-bounds-check
|
||||
DEBUG_COMMON_FLAGS+= -ggdb -gz -ftrapv -fbounds-check
|
||||
|
||||
ifneq ($(TARGET_SPEC_FLAGS),no)
|
||||
RELEASE_CFLAGS?= -O3 -flto $(OPT_FLAGS)
|
||||
RELEASE_CXXFLAGS?= -O3 -flto $(CXX_OPT_FLAGS)
|
||||
RELEASE_LDFLAGS?= -Wl,-O3 -Wl,-flto
|
||||
|
||||
DEBUG_CFLAGS?= -Og
|
||||
DEBUG_CXXFLAGS?= -Og
|
||||
|
||||
DEBUG_LDFLAGS?=
|
||||
endif
|
||||
|
||||
ifeq ($(PARALLEL),release)
|
||||
# Add openmp to C & CXX *only* in release mode.
|
||||
RELEASE_COMMON_FLAGS+= -fopenmp -DPARALLEL=1
|
||||
|
||||
RELEASE_LDFLAGS+= -ltbb -lgomp
|
||||
endif
|
||||
|
||||
# Set exception modes (See def `EXCEPTIONS`.)
|
||||
|
||||
ifneq ($(EXCEPTIONS),)
|
||||
ifeq ($(EXCEPTIONS),debug)
|
||||
# XXX (see above) Should we change to `DEBUG_COMMON_FLAGS` here to pass to CXX as well?
|
||||
DEBUG_CFLAGS+=-fexceptions
|
||||
endif
|
||||
|
||||
ifeq ($(EXCEPTIONS),yes)
|
||||
CFLAGS+=-fexceptions
|
||||
endif
|
||||
|
||||
ifeq ($(EXCEPTIONS),no)
|
||||
CXXFLAGS+=-fno-exceptions
|
||||
endif
|
||||
|
||||
ifeq ($(EXCEPTIONS),force)
|
||||
COMMON_FLAGS+=-fexceptions
|
||||
endif
|
||||
|
||||
ifeq ($(EXCEPTIONS),never)
|
||||
COMMON_FLAGS+=-fno-exceptions
|
||||
endif
|
||||
endif
|
||||
|
||||
DEBUG_CFLAGS+=-DDEBUG $(DEBUG_COMMON_FLAGS)
|
||||
DEBUG_CXXFLAGS+=-DDEBUG $(DEBUG_COMMON_FLAGS) -fasynchronous-unwind-tables
|
||||
|
||||
RELEASE_CFLAGS+=-DRELEASE $(RELEASE_COMMON_FLAGS)
|
||||
RELEASE_CXXFLAGS+=-DRELEASE $(RELEASE_COMMON_FLAGS)
|
||||
|
||||
# Objects
|
||||
|
||||
OBJ_C = $(addprefix obj/c/,$(SRC_C:.c=.o))
|
||||
OBJ_CXX = $(addprefix obj/cxx/,$(SRC_CXX:.cpp=.o))
|
||||
OBJ = $(OBJ_C) $(OBJ_CXX)
|
||||
|
||||
# Pre-compiled header objects
|
||||
|
||||
PCH_HEADERS+=$(shell find -O2 $(INCLUDE) -type f -name \*_p.hh -or -name \*_p.h)
|
||||
|
||||
PCH_OUTPUT_LOCATION=
|
||||
PCH_OUTPUT=$(addprefix $(PCH_OUTPUT_LOCATION),$(addsuffix .gch,$(PCH_HEADERS)))
|
||||
|
||||
PCH_BUILD_COMMON_FLAGS+=-H
|
||||
#-D_PCH_BUILD=1
|
||||
|
||||
PCH_BUILD_CFLAGS+= -x c-header $(PCH_BUILD_COMMON_FLAGS)
|
||||
PCH_BUILD_CXXFLAGS+= -x c++-header $(PCH_BUILD_COMMON_FLAGS)
|
||||
|
||||
PCH_CHEADERS=$(filter %.h,$(PCH_HEADERS))
|
||||
PCH_CXXHEADERS=$(filter %.hh,$(PCH_HEADERS))
|
||||
|
||||
PCH_USE_COMMON_FLAGS+=-Winvalid-pch
|
||||
|
||||
ifeq ($(INCLUDE_PCH_GLOBAL),1)
|
||||
# Globally included
|
||||
PCH_USE_CFLAGS+= $(addprefix -include ,$(realpath $(PCH_CHEADERS)))
|
||||
PCH_USE_CXXFLAGS+= $(addprefix -include ,$(realpath $(PCH_CXXHEADERS)))
|
||||
else
|
||||
# Seperately include them
|
||||
ifneq ($(PCH_OUTPUT_LOCATION),)
|
||||
override PCH_OUTPUT_LOCATION:=$(dir $(PCH_OUTPUT_LOCATION))
|
||||
# XXX: We don't wan't to be using PCH_OUTPUT_LOCATION really...
|
||||
COMMON_FLAGS:= $(addprefix -I,$(dir $(shell find -O3 $(PCH_OUTPUT_LOCATION) -mindepth 1 -type d -printf %p/ ))) $(COMMON_FLAGS)
|
||||
endif
|
||||
endif
|
||||
|
||||
COMMON_FLAGS+=$(addprefix -include ,$(INCLUDE_GLOBAL))
|
||||
|
||||
PCH_USE_CFLAGS+= $(PCH_USE_COMMON_FLAGS)
|
||||
PCH_USE_CXXFLAGS+= $(PCH_USE_COMMON_FLAGS)
|
||||
|
||||
# Compiler Flags
|
||||
|
||||
CFLAGS += $(COMMON_FLAGS) --std=$(CSTD)
|
||||
CXXFLAGS += $(COMMON_FLAGS) --std=$(CXXSTD)
|
||||
LDFLAGS += $(addsuffix .a,$(addprefix -l:lib,$(STATIC_LIBS))) $(addprefix -l,$(SHARED_LIBS))
|
||||
|
||||
# PGO (unused for lib targets)
|
||||
|
||||
PROF_FLAGS= -D_PGO_GEN -fprofile-generate
|
||||
PGO_OBJ_C= $(addprefix prof/c/,$(SRC_C:.c=.o))
|
||||
PGO_OBJ_CXX= $(addprefix prof/cxx/,$(SRC_CXX:.c=.o))
|
||||
PGO_OBJ= $(PGO_OBJ_C) $(PGO_OBJ_CXX)
|
||||
|
||||
PROF_ITERATIONS=10
|
||||
PROF_LOCATION?=/tmp/$(PROJECT)-pgo
|
||||
PROF_LARGE_BOUND= 10240
|
||||
PROF_SMALL_BOUND= 1024
|
||||
|
||||
# Phonies
|
||||
|
||||
# XXX: This doesn't force them to run in series for some reason?
|
||||
|
||||
.PHONY: release
|
||||
release: | dirs
|
||||
$(MAKE) lib$(PROJECT).a
|
||||
@$(MAKE) clean-rebuild >> /dev/null
|
||||
@$(MAKE) dirs >> /dev/null
|
||||
$(MAKE) lib$(PROJECT).so
|
||||
|
||||
.PHONY: debug
|
||||
debug: | dirs
|
||||
$(MAKE) lib$(PROJECT)-debug.a
|
||||
@$(MAKE) clean-rebuild >> /dev/null
|
||||
@$(MAKE) dirs >> /dev/null
|
||||
$(MAKE) lib$(PROJECT)-debug.so
|
||||
|
||||
# Rebuild both release and debug targets from scratch
|
||||
.PHONY: all
|
||||
all: | clean
|
||||
@$(MAKE) release
|
||||
@$(MAKE) clean-rebuild
|
||||
@$(MAKE) debug
|
||||
|
||||
.PHONY: test
|
||||
test: $(PROJECT)-test
|
||||
-strace ./$<
|
||||
-valgrind ./$<
|
||||
|
||||
# Targets
|
||||
|
||||
.PHONY: pch
|
||||
pch: | dirs $(PROJECT)-pch
|
||||
@echo 'WARNING: When building target $(PROJECT)-pch, the PCH file(s) will not be built with the default auto-opts enabled by targets of kind `debug` or `release`. This should only be used when full manually overriding all compile and linker flags' >&2
|
||||
|
||||
$(PROJECT)-pch: $(PCH_OUTPUT)
|
||||
@echo ""
|
||||
@echo 'PCH: $(PCH_OUTPUT)' '<-' '$(PCH_HEADERS)'
|
||||
@echo " with: $(PCH_INCLUDES)"
|
||||
@echo 'C: $(PCH_CHEADERS)'
|
||||
@echo 'C++: $(PCH_CXXHEADERS)'
|
||||
# Invoking target `pch` itself is not necissary, they are build automatically where they are used.
|
||||
|
||||
dirs:
|
||||
@mkdir -p $(addprefix {obj$(__COMMA)prof}/c{$(__COMMA)xx}/,$(shell find $(SRC)/ -type d))
|
||||
|
||||
|
||||
$(PCH_OUTPUT_LOCATION)%.hh.gch: CXXFLAGS+=$(PCH_BUILD_CXXFLAGS)
|
||||
$(PCH_OUTPUT_LOCATION)%.hh.gch: %.hh $(PCH_INCLUDES)
|
||||
@mkdir -p $(dir $@)
|
||||
$(CXX) $(CXXFLAGS) -o $@ -c $<
|
||||
|
||||
$(PCH_OUTPUT_LOCATION)%.h.gch: CFLAGS+=$(PCH_BUILD_CFLAGS)
|
||||
$(PCH_OUTPUT_LOCATION)%.h.gch: %.h $(PCH_INCLUDES)
|
||||
@mkdir -p $(dir $@)
|
||||
$(CC) $(CFLAGS) -o $@ -c $<
|
||||
|
||||
obj/c/%.o: CFLAGS+= $(PCH_USE_CFLAGS)
|
||||
obj/c/%.o: %.c $(PCH_OUTPUT)
|
||||
$(CC) -c $< $(CFLAGS) -o $@
|
||||
|
||||
obj/cxx/%.o: CXXFLAGS+= $(PCH_USE_CXXFLAGS)
|
||||
obj/cxx/%.o: %.cpp $(PCH_OUTPUT)
|
||||
$(CXX) -c $< $(CXXFLAGS) -o $@
|
||||
|
||||
prof/c/%.o: CFLAGS+= $(PCH_USE_CFLAGS)
|
||||
prof/c/%.o: %.c $(PCH_OUTPUT)
|
||||
$(CC) -c $< $(CFLAGS) -o $@ $(PROF_FLAGS)
|
||||
#$(LDFLAGS)
|
||||
|
||||
prof/cxx/%.o: CXXFLAGS+= $(PCH_USE_CXXFLAGS)
|
||||
prof/cxx/%.o: %.cpp $(PCH_OUTPUT)
|
||||
$(CXX) -c $< $(CXXFLAGS) -o $@ $(PROF_FLAGS)
|
||||
#$(LDFLAGS)
|
||||
|
||||
lib$(PROJECT)-release.a: CFLAGS+= $(RELEASE_CFLAGS) $(STATIC_FLAGS) $(STATIC_RELEASE_FLAGS)
|
||||
lib$(PROJECT)-release.a: CXXFLAGS += $(RELEASE_CXXFLAGS) $(STATIC_FLAGS) $(STATIC_RELEASE_FLAGS)
|
||||
lib$(PROJECT)-release.a: LDFLAGS += $(RELEASE_LDFLAGS)
|
||||
lib$(PROJECT)-release.a: $(OBJ)
|
||||
$(AR) rcs $@ $^
|
||||
$(RANLIB) $@
|
||||
|
||||
lib$(PROJECT)-debug.a: CFLAGS+= $(DEBUG_CFLAGS) $(STATIC_FLAGS) $(STATIC_DEBUG_FLAGS)
|
||||
lib$(PROJECT)-debug.a: CXXFLAGS += $(DEBUG_CXXFLAGS) $(STATIC_FLAGS) $(STATIC_DEBUG_FLAGS)
|
||||
lib$(PROJECT)-debug.a: LDFLAGS += $(DEBUG_LDFLAGS)
|
||||
lib$(PROJECT)-debug.a: $(OBJ)
|
||||
$(AR) rcs $@ $^
|
||||
$(RANLIB) $@
|
||||
|
||||
lib$(PROJECT)-release.so: CFLAGS+= $(RELEASE_CFLAGS) $(SHARED_FLAGS) $(SHARED_RELEASE_FLAGS)
|
||||
lib$(PROJECT)-release.so: CXXFLAGS += $(RELEASE_CXXFLAGS) $(SHARED_FLAGS) $(SHARED_RELEASE_FLAGS)
|
||||
lib$(PROJECT)-release.so: LDFLAGS += $(RELEASE_LDFLAGS)
|
||||
lib$(PROJECT)-release.so: BINFLAGS += $(RELEASE_BINFLAGS)
|
||||
lib$(PROJECT)-release.so: $(OBJ)
|
||||
$(CXX) -shared $^ $(BINFLAGS) $(CXXFLAGS) -o $@ $(LDFLAGS)
|
||||
$(STRIP) $@
|
||||
|
||||
lib$(PROJECT)-debug.so: CFLAGS+= $(DEBUG_CFLAGS) $(SHARED_FLAGS) $(SHARED_DEBUG_FLAGS)
|
||||
lib$(PROJECT)-debug.so: CXXFLAGS += $(DEBUG_CXXFLAGS) $(SHARED_FLAGS) $(SHARED_DEBUG_FLAGS)
|
||||
lib$(PROJECT)-debug.so: LDFLAGS += $(DEBUG_LDFLAGS)
|
||||
lib$(PROJECT)-debug.so: BINFLAGS += $(DEBUG_BINFLAGS)
|
||||
lib$(PROJECT)-debug.so: $(OBJ)
|
||||
$(CXX) -shared $^ $(BINFLAGS) $(CXXFLAGS) -o $@ $(LDFLAGS)
|
||||
|
||||
lib$(PROJECT).a: lib$(PROJECT)-release.a
|
||||
ln -f $< $@
|
||||
|
||||
lib$(PROJECT).so: LDFLAGS+= -Wl,-soname,lib$(PROJECT).so.$(VERSION_MAJOR)
|
||||
lib$(PROJECT).so: lib$(PROJECT)-release.so
|
||||
ln -f $< $@.$(VERSION)
|
||||
ln -sf $@.$(VERSION) $@.$(VERSION_MAJOR)
|
||||
ln -sf $@.$(VERSION_MAJOR) $@
|
||||
|
||||
clean-source:
|
||||
find -O2 {obj,prof}/ -type f -exec rm {} +
|
||||
|
||||
clean-rebuild: clean-source
|
||||
find $(INCLUDE) $(PCH_OUTPUT_LOCATION) -type f -name \*.gch -exec rm {} +
|
||||
|
||||
|
||||
clean: clean-rebuild
|
||||
rm -f lib$(PROJECT){,-{release,debug,pgo}}.{a,so{,.*}}
|
||||
rm -f $(PROJECT)-test
|
||||
|
||||
clean-full: clean
|
||||
rm -rf {obj,prof}
|
||||
|
||||
install:
|
||||
install -d $(DESTDIR)$(PREFIX)/lib/
|
||||
install -m 644 lib$(PROJECT).a $(DESTDIR)$(PREFIX)/lib/
|
||||
install -s -m 755 lib$(PROJECT).so.$(VERSION) $(DESTDIR)$(PREFIX)/lib/
|
||||
ln -sf lib$(PROJECT).so.$(VERSION) $(DESTDIR)$(PREFIX)/lib/lib$(PROJECT).so.$(VERSION_MAJOR)
|
||||
ln -sf lib$(PROJECT).so.$(VERSION_MAJOR) $(DESTDIR)$(PREFIX)/lib/lib$(PROJECT).so
|
||||
install -d $(DESTDIR)$(PREFIX)/include/$(INCLUDE_PREFIX)
|
||||
install -m 644 $(wildcard $(INCLUDE)/*.*) $(DESTDIR)$(PREFIX)/include/$(INCLUDE_PREFIX)
|
||||
uninstall:
|
||||
-rm $(DESTDIR)$(PREFIX)/lib/lib$(PROJECT).{a,so{,.*}}
|
||||
cd $(INCLUDE) && find . -type f | xargs -I {} rm "$(DESTDIR)$(PREFIX)/include/$(INCLUDE_PREFIX){}"
|
||||
[[ -d "$(DESTDIR)$(PREFIX)/include/$(INCLUDE_PREFIX)" ]] && \
|
||||
rmdir $(DESTDIR)$(PREFIX)/include/$(INCLUDE_PREFIX) || :
|
||||
|
||||
$(PROJECT)-test: lib$(PROJECT)-$(TARGET)
|
||||
$(CXX) $(CXXFLAGS) $(TEST_CFLAGS) $(TEST_CXXFLAGS) src/test/*.cpp -o $@ -l:$< $(LDFLAGS) $(TEST_LDFLAGS)
|
@ -0,0 +1,188 @@
|
||||
//! A value-wrapping condvar, controlled via mutex / rw-lock
|
||||
#pragma once
|
||||
|
||||
#include <condition_variable>
|
||||
|
||||
#include "error.hh"
|
||||
#include "mutex.hh"
|
||||
|
||||
namespace fx::condvar {
|
||||
template< typename Mutex, typename T = void>
|
||||
struct mutex_cond;
|
||||
|
||||
template<typename T>
|
||||
struct mutex_cond<std::shared_mutex, T> {
|
||||
using wrapper_type = shared::Mutex<T>;
|
||||
using condvar_type = std::condition_variable_any;
|
||||
using mutex_type = std::shared_mutex;
|
||||
};
|
||||
|
||||
template<typename T>
|
||||
struct mutex_cond<std::mutex, T> {
|
||||
using wrapper_type = Mutex<T>;
|
||||
using condvar_type = std::condition_variable;
|
||||
using mutex_type = std::mutex;
|
||||
};
|
||||
|
||||
template<typename T>
|
||||
struct mutex_cond<Mutex<T>, T> : mutex_cond<std::mutex, T> {};
|
||||
|
||||
template<typename T>
|
||||
struct mutex_cond<shared::Mutex<T>, T> : mutex_cond<std::shared_mutex, T> {};
|
||||
|
||||
/*template<typename M, typename T>
|
||||
struct mutex_cond<mutex::Mutex<M, T>, T> {
|
||||
using wrapper_type = mutex::Mutex<M, T>;
|
||||
using mutex_type = typename wrapper_type::mutex_type;
|
||||
};*/
|
||||
|
||||
template<typename M>
|
||||
concept CondMutex = requires{ typename mutex_cond<M>::condvar_type; };
|
||||
|
||||
template<typename M, typename T>
|
||||
struct CondVar : public mutex_cond<M, T>::wrapper_type {
|
||||
using condvar_type = mutex_cond<M, T>::condvar_type;
|
||||
|
||||
using mutex_cond<M, T>::wrapper_type::wrapper_type;
|
||||
using mutex_cond<M, T>::wrapper_type::with_lock;
|
||||
|
||||
inline void notify_one() { m_cond.notify_one(); }
|
||||
inline void notify_all() { m_cond.notify_all(); }
|
||||
|
||||
template<typename F>
|
||||
requires(std::is_invocable_v<F, T&>)
|
||||
inline decltype(auto) wait_then(F&& func)
|
||||
noexcept(std::is_nothrow_invocable_v<F, T&>)
|
||||
{
|
||||
std::unique_lock ulk{ this->m_mutex };
|
||||
m_cond.wait(ulk);
|
||||
// Unique lock makes this raw access safe
|
||||
return std::forward<F>(func)(*this->get_raw_ptr());
|
||||
}
|
||||
|
||||
template<typename F>
|
||||
inline decltype(auto) wait_then(F&& func, auto const& predicate)
|
||||
noexcept(std::is_nothrow_invocable_v<F, T&> and std::is_nothrow_invocable_v<decltype(predicate), T const&>)
|
||||
requires(std::is_invocable_v<F, T&> and std::is_invocable_v<decltype(predicate), T const&>)
|
||||
{
|
||||
std::unique_lock ulk{ this->m_mutex };
|
||||
m_cond.wait(ulk, [&, this] { return predicate(*this->get_ptr()); });
|
||||
// Unique lock makes this raw access safe
|
||||
return std::forward<F>(func)(*this->get_raw_ptr());
|
||||
|
||||
}
|
||||
|
||||
inline
|
||||
condvar_type& get_condvar() noexcept { return m_cond; }
|
||||
|
||||
inline
|
||||
condvar_type const& get_condvar() const noexcept { return m_cond; }
|
||||
protected:
|
||||
condvar_type m_cond{};
|
||||
};
|
||||
}
|
||||
|
||||
namespace fx {
|
||||
template<typename T>
|
||||
struct CondVar final : condvar::CondVar<std::mutex, T> {
|
||||
using condvar::CondVar<std::mutex, T>::CondVar;
|
||||
|
||||
// using condvar::CondVar<std::mutex, T>::try_wait_then; // Only available for `shared::CondVar`
|
||||
using condvar::CondVar<std::mutex, T>::wait_then;
|
||||
};
|
||||
|
||||
namespace shared {
|
||||
template<typename T>
|
||||
struct CondVar final : condvar::CondVar<std::shared_mutex, T> {
|
||||
using condvar::CondVar<std::shared_mutex, T>::CondVar;
|
||||
|
||||
using condvar::CondVar<std::shared_mutex, T>::wait_then;
|
||||
|
||||
template<typename F>
|
||||
inline bool try_wait_then(F&& func, auto const& predicate, std::stop_token st = std::stop_token{})
|
||||
noexcept(std::is_nothrow_invocable_v<F, T&> and std::is_nothrow_invocable_v<decltype(predicate), T const&>)
|
||||
requires(std::is_invocable_v<F, T&> and std::is_invocable_v<decltype(predicate), T const&>)
|
||||
{
|
||||
std::unique_lock ulk{ this->m_mutex };
|
||||
if(! this->m_cond.wait(ulk, st, [&, this] { return predicate(*this->get_ptr()); }))
|
||||
// Abandon attempt at locking
|
||||
return false;
|
||||
// Unique lock makes raw access safe
|
||||
else if constexpr(std::is_convertible_v<std::invoke_result_t<F, T&>, bool>)
|
||||
return std::forward<F>(func)(*this->get_raw_ptr());
|
||||
else std::forward<F>(func)(*this->get_raw_ptr());
|
||||
return true;
|
||||
}
|
||||
template<typename F>
|
||||
inline auto wait_then(F&& func, auto const& predicate, std::stop_token st)
|
||||
noexcept(std::is_nothrow_invocable_v<F, T&> and std::is_nothrow_invocable_v<decltype(predicate), T const&>)
|
||||
requires(std::is_invocable_v<F, T&> and std::is_invocable_v<decltype(predicate), T const&>)
|
||||
{
|
||||
std::unique_lock ulk{ this->m_mutex };
|
||||
if(! this->m_cond.wait(ulk, st, [&, this] { return predicate(*this->get_ptr()); }))
|
||||
error::throw_cancelled();
|
||||
|
||||
return std::forward<F>(func)(*this->get_raw_ptr());
|
||||
}
|
||||
|
||||
|
||||
|
||||
//NOTE: _shared() cannot be `const` because `m_cond.wait()` is not `const`. It also takes a signal (`notify_one()`) permit regardless of how that wake is used, so.
|
||||
template<typename F>
|
||||
requires(std::is_invocable_v<F, const T&>)
|
||||
inline decltype(auto) wait_then_shared(F&& func)
|
||||
noexcept(std::is_nothrow_invocable_v<F, const T&>)
|
||||
{
|
||||
std::shared_lock ulk{ this->m_mutex };
|
||||
this->m_cond.wait(ulk);
|
||||
// Shared lock makes this only `const` access safe
|
||||
return std::forward<F>(func)(*this->get_ptr());
|
||||
}
|
||||
|
||||
template<typename F>
|
||||
inline decltype(auto) wait_then_shared(F&& func, auto const& predicate)
|
||||
noexcept(std::is_nothrow_invocable_v<F, const T&> and std::is_nothrow_invocable_v<decltype(predicate), T const&>)
|
||||
requires(std::is_invocable_v<F, const T&> and std::is_invocable_v<decltype(predicate), T const&>)
|
||||
{
|
||||
std::shared_lock ulk{ this->m_mutex };
|
||||
this->m_cond.wait(ulk, [&, this] { return predicate(*this->get_ptr()); });
|
||||
// Shared lock makes this only `const` access safe
|
||||
return std::forward<F>(func)(*this->get_ptr());
|
||||
|
||||
}
|
||||
|
||||
template<typename F>
|
||||
inline bool try_wait_then_shared(F&& func, auto const& predicate, std::stop_token st = std::stop_token{})
|
||||
noexcept(std::is_nothrow_invocable_v<F, const T&> and std::is_nothrow_invocable_v<decltype(predicate), T const&>)
|
||||
requires(std::is_invocable_v<F, const T&> and std::is_invocable_v<decltype(predicate), T const&>)
|
||||
{
|
||||
std::shared_lock ulk{ this->m_mutex };
|
||||
if(! this->m_cond.wait(ulk, st, [&, this] { return predicate(*this->get_ptr()); }))
|
||||
// Abandon attempt at locking
|
||||
return false;
|
||||
// Shared lock makes only `const` access safe
|
||||
else if constexpr(std::is_convertible_v<std::invoke_result_t<F, T const&>, bool>)
|
||||
return std::forward<F>(func)(*this->get_ptr());
|
||||
else std::forward<F>(func)(*this->get_ptr());
|
||||
return true;
|
||||
}
|
||||
|
||||
template<typename F>
|
||||
inline decltype(auto) wait_then_shared(F&& func, auto const& predicate, std::stop_token st)
|
||||
noexcept(std::is_nothrow_invocable_v<F, const T&> and std::is_nothrow_invocable_v<decltype(predicate), T const&>)
|
||||
requires(std::is_invocable_v<F, const T&> and std::is_invocable_v<decltype(predicate), T const&>)
|
||||
{
|
||||
std::shared_lock ulk{ this->m_mutex };
|
||||
if(! this->m_cond.wait(ulk, st, [&, this] { return predicate(*this->get_ptr()); }))
|
||||
error::throw_cancelled();
|
||||
|
||||
return std::forward<F>(func)(*this->get_ptr());
|
||||
}
|
||||
|
||||
};
|
||||
}
|
||||
template<typename T>
|
||||
using SharedCondVar = shared::CondVar<T>;
|
||||
}
|
||||
|
||||
|
@ -0,0 +1,56 @@
|
||||
//! Sync errors
|
||||
#pragma once
|
||||
|
||||
#include <stop_token>
|
||||
#include <stdexcept>
|
||||
|
||||
namespace fx::error {
|
||||
/// Thrown when an operation is aborted via a stop signal.
|
||||
///
|
||||
/// The state may be inconsistent after the thrown exception.
|
||||
struct OperationCancelledError : virtual std::runtime_error {
|
||||
OperationCancelledError() noexcept;
|
||||
OperationCancelledError(OperationCancelledError const&) noexcept = default;
|
||||
OperationCancelledError& operator=(OperationCancelledError const&) noexcept = default;
|
||||
|
||||
virtual ~OperationCancelledError();
|
||||
};
|
||||
|
||||
/// Thrown when a task is cancelled via a stop signal.
|
||||
///
|
||||
/// The state is **not** left in an inconsistent state after the thrown exception.
|
||||
struct TaskCancelledError final : OperationCancelledError {
|
||||
TaskCancelledError() noexcept;
|
||||
TaskCancelledError(TaskCancelledError const&) noexcept = default;
|
||||
TaskCancelledError& operator=(TaskCancelledError const&) noexcept = default;
|
||||
|
||||
virtual ~TaskCancelledError();
|
||||
};
|
||||
|
||||
// Throw an `OperationCancelledError` (or, optionally, a `TaskCancelledError` if there is a completed task that was cancelled instead of an operation being aborted.)
|
||||
[[gnu::noinline, gnu::cold, noreturn]]
|
||||
void throw_cancelled(bool clean = false);
|
||||
|
||||
[[gnu::gnu_inline]]
|
||||
extern inline
|
||||
void throw_if_cancelled(std::stop_token const& st, bool clean = false) {
|
||||
if(not st.stop_requested()) return;
|
||||
else
|
||||
throw_cancelled(clean);
|
||||
}
|
||||
|
||||
namespace details [[gnu::visibility("internal")]] {
|
||||
// NOTE: This assumes the existence of `extern "C" void ::throw_last_system_error()` which throws the current `errno` as a generic-categoried `std::system_error`. If that does not exist, it is re-implemented below. (XXX: There is no reason for this now? We have an impl to call... Ffs...) //
|
||||
static void throw_last_system_error() __attribute__((cold, noreturn, weakref("throw_last_system_error")));
|
||||
}
|
||||
|
||||
[[gnu::gnu_inline, gnu::cold, noreturn]]
|
||||
extern inline
|
||||
void throw_last_system_error()
|
||||
{
|
||||
if(__builtin_expect(details::throw_last_system_error != nullptr, true))
|
||||
details::throw_last_system_error();
|
||||
else (&throw_last_system_error)();
|
||||
__builtin_unreachable();
|
||||
}
|
||||
}
|
@ -0,0 +1,276 @@
|
||||
//! A value-wrapping mutex / rw-lock
|
||||
#pragma once
|
||||
#include <mutex>
|
||||
#include <shared_mutex>
|
||||
#include <memory>
|
||||
#include <utility>
|
||||
#include <concepts>
|
||||
|
||||
namespace fx::mutex {
|
||||
template<typename F, typename T>
|
||||
concept Delegate = std::is_invocable_v<std::add_rvalue_reference_t<F>, T>;
|
||||
|
||||
template<typename T>
|
||||
struct storage_traits {
|
||||
using value_type = std::decay_t<T>;
|
||||
typedef std::unique_ptr<value_type> storage_type;
|
||||
|
||||
template<typename... Args>
|
||||
requires(std::is_constructible_v<value_type, Args...>)
|
||||
constexpr static storage_type make_storage(Args&&... args) noexcept(std::is_nothrow_constructible_v<value_type, Args...>)
|
||||
{
|
||||
return std::make_unique<value_type>(std::forward<Args>(args)...);
|
||||
}
|
||||
|
||||
constexpr static storage_type make_storage() noexcept(std::is_nothrow_default_constructible_v<value_type>) requires(std::is_default_constructible_v<value_type>)
|
||||
{ return std::make_unique<value_type>(); }
|
||||
|
||||
};
|
||||
|
||||
namespace details [[gnu::visibility("hidden")]] {
|
||||
template<typename T>
|
||||
concept has_storage = requires{ typename storage_traits<T>::storage_type; };
|
||||
|
||||
template<typename T>
|
||||
concept has_valid_storage = has_storage<T> and requires(storage_traits<T>::storage_type const& value) {
|
||||
{ value.get() } noexcept -> std::convertible_to< typename std::add_pointer_t<typename storage_traits<T>::value_type> >;
|
||||
};
|
||||
|
||||
template<typename T, typename... Args>
|
||||
concept can_construct = has_storage<T> and requires(Args&&... args) {
|
||||
{ storage_traits<T>::template make_storage<Args...>(std::forward<Args>(args)...) }
|
||||
-> std::convertible_to< typename storage_traits<T>::storage_type >;
|
||||
};
|
||||
template<typename T>
|
||||
concept can_default_construct = has_storage<T> and requires{
|
||||
{ storage_traits<T>::make_storage() }
|
||||
-> std::convertible_to< typename storage_traits<T>::storage_type >;
|
||||
};
|
||||
}
|
||||
|
||||
template<typename M, typename T>
|
||||
class Mutex {
|
||||
static_assert(details::has_valid_storage<T>, "Invalid storage_traits<T> spec for stored type");
|
||||
|
||||
using value_type = storage_traits<T>::value_type;
|
||||
using storage_type = storage_traits<T>::storage_type;
|
||||
|
||||
|
||||
storage_type m_value;
|
||||
public:
|
||||
using mutex_type = std::decay_t<M>;
|
||||
|
||||
constexpr explicit Mutex(std::convertible_to<storage_type> auto&& up) noexcept
|
||||
: m_value(std::move(up)) {}
|
||||
|
||||
constexpr explicit Mutex() noexcept requires(details::can_default_construct<T>)
|
||||
: m_value(storage_traits<T>::make_storage()) {}
|
||||
constexpr Mutex(T&& value) noexcept(std::is_nothrow_move_constructible_v<value_type>)
|
||||
: m_value(storage_traits<T>::make_storage(std::forward<T>(value)))
|
||||
//, m_mutex(std::forward<M>(mutex))
|
||||
{}
|
||||
|
||||
Mutex(const Mutex&) = delete;
|
||||
Mutex(Mutex&&) = delete;
|
||||
Mutex& operator=(Mutex const&) = delete;
|
||||
Mutex& operator=(Mutex &&) = delete;
|
||||
|
||||
[[gnu::returns_nonnull, gnu::artificial]]
|
||||
inline T* get_ptr_unsafe() const noexcept { return get_raw_ptr(); }
|
||||
[[gnu::returns_nonnull, gnu::artificial]]
|
||||
inline T const* get_ptr() const noexcept { return static_cast<T const*>(get_raw_ptr()); }
|
||||
|
||||
inline virtual mutex_type& get_mutex() const noexcept { return m_mutex; }
|
||||
protected:
|
||||
[[gnu::returns_nonnull]]
|
||||
inline virtual value_type* get_raw_ptr() const noexcept {
|
||||
value_type* p = m_value.get();
|
||||
if(!p) __builtin_unreachable();
|
||||
return p;
|
||||
}
|
||||
|
||||
inline T& value() & noexcept { return *get_raw_ptr(); }
|
||||
inline T const& value() const& noexcept { return *get_raw_ptr(); }
|
||||
inline T&& value() && noexcept { return std::move(*get_raw_ptr()); }
|
||||
inline T const&& value() const&& noexcept { return std::move(*get_raw_ptr()); }
|
||||
|
||||
|
||||
mutable mutex_type m_mutex{};
|
||||
};
|
||||
|
||||
/// For callback passed to `with_lock()` & like functions that copies & moves the value of type `T`.
|
||||
template<typename T>
|
||||
constexpr auto copy_value(T const& v) noexcept(std::is_nothrow_copy_constructible_v<T>)
|
||||
{
|
||||
return v;
|
||||
}
|
||||
|
||||
template<typename T>
|
||||
constexpr auto move_value(T& v) noexcept(std::is_nothrow_move_constructible_v<T>)
|
||||
{
|
||||
return std::move(v);
|
||||
}
|
||||
|
||||
/// For callback passed to `with_lock()` & like functions that does nothing and returns `void`.
|
||||
[[gnu::artificial, gnu::always_inline]]
|
||||
constexpr void nothing(const auto&) noexcept {}
|
||||
|
||||
[[gnu::artificial, gnu::always_inline]]
|
||||
constexpr void nothing(auto&) noexcept {}
|
||||
|
||||
/// Creates a callback for `with_lock()` & like functions that returns an already given value of type `T`.
|
||||
///
|
||||
/// If `T` is not trivially-copy-constructible, the value returned from the callback will be move-constructed from the captured `value`.
|
||||
/// `value` is moved into the callback via a forward to a decayed `U`.
|
||||
/// If the wish is to pass the rvalue-reference down the chain, having `with_lock()` return the moved value directly, use `return_ref<T>()` instead.
|
||||
template<typename T, std::convertible_to<T> U = T>
|
||||
constexpr auto return_value(U&& value) noexcept(std::is_nothrow_convertible_v<U, T>) {
|
||||
if constexpr(std::is_trivially_copy_constructible_v<T>) {
|
||||
return [value = std::forward<U>(value)] (auto const&) noexcept(std::is_nothrow_copy_constructible_v<T>) -> T {
|
||||
return value;
|
||||
};
|
||||
} else
|
||||
return [value = std::forward<U>(value)] (auto const&) mutable noexcept(std::is_nothrow_move_constructible_v<T>) -> T {
|
||||
return std::move(value);
|
||||
};
|
||||
}
|
||||
|
||||
/// Creates a callback for `with_lock()` & like functions that passes a value *through* the callback and constructs it when the callback returns.
|
||||
/// So an rvalue-reference given to `.with_lock(return_ref<T>(std::move(obj)))` will construct the object `T` from a forwarding-reference of the passed rvalue-reference `std::move(obj)` only at the *end* of `.with_lock()` when it returns before releasing the lock.)
|
||||
///
|
||||
/// This is in contrast to `return_value<T>()`, which captures the passed forwarding-reference *by value* in the callback itself and moves out of it when the callback returns, so the above example would construct `T` from an intermediary `U` which is the decayed-type of the expression `std::move(obj)`. T
|
||||
/// herefore a potential 2nd construction is avoided with this method, however the lifetime of `value` must be taken into account when using this callback, as `value` is captured only by forwarding its reference.
|
||||
///
|
||||
/// Unlike `return_value<T>()`, The lifetime of the returned callback is bound to `U`.
|
||||
template<typename T, std::convertible_to<T> U = T>
|
||||
constexpr auto return_ref(U&& value) noexcept {
|
||||
return [&] (auto const&) noexcept(std::is_nothrow_convertible_v<U, T>) -> T { return std::forward<U>(value); };
|
||||
}
|
||||
}
|
||||
|
||||
namespace fx {
|
||||
template<typename T>
|
||||
struct Mutex : public mutex::Mutex<std::mutex, T> {
|
||||
using mutex::Mutex<std::mutex, T>::Mutex;
|
||||
|
||||
template<mutex::Delegate<T&> F>
|
||||
inline decltype(auto) with_lock(F&& f) & noexcept(std::is_nothrow_invocable_v<F, T&>)
|
||||
{
|
||||
std::unique_lock ul{ this->get_mutex() };
|
||||
return std::forward<F>(f)(this->value());
|
||||
}
|
||||
|
||||
template<mutex::Delegate<T const&> F>
|
||||
inline decltype(auto) with_lock(F&& f) const& noexcept(std::is_nothrow_invocable_v<F, T const&>)
|
||||
{
|
||||
std::unique_lock ul{ this->get_mutex() };
|
||||
return std::forward<F>(f)(this->value());
|
||||
}
|
||||
|
||||
template<mutex::Delegate<T&&> F>
|
||||
inline decltype(auto) with_lock(F&& f) && noexcept(std::is_nothrow_invocable_v<F, T&&>)
|
||||
{
|
||||
// Assume this is owned by caller. No lock required.
|
||||
//std::unique_lock ul{ this->get_mutex() };
|
||||
return std::forward<F>(f)(std::move(this->value()));
|
||||
}
|
||||
|
||||
template<mutex::Delegate<T const&&> F>
|
||||
inline decltype(auto) with_lock(F&& f) const&& noexcept(std::is_nothrow_invocable_v<F, T const&&>)
|
||||
{
|
||||
std::unique_lock ul{ this->get_mutex() };
|
||||
return std::forward<F>(f)(std::move(this->value()));
|
||||
}
|
||||
|
||||
template<mutex::Delegate<T> F>
|
||||
inline decltype(auto) with_unique_lock(F&& f) const noexcept(std::is_nothrow_invocable_v<F, T>)
|
||||
{
|
||||
std::unique_lock ul{ this->get_mutex() };
|
||||
if constexpr(std::is_invocable_v<decltype(f), T, decltype(ul)>) {
|
||||
return std::forward<F>(f)(*this->get_raw_ptr(), ul);
|
||||
} else return std::forward<F>(f)(*this->get_raw_ptr());
|
||||
}
|
||||
};
|
||||
|
||||
namespace shared {
|
||||
template<typename T>
|
||||
struct Mutex : public mutex::Mutex<std::shared_mutex, T> {
|
||||
using mutex::Mutex<std::shared_mutex, T>::Mutex;
|
||||
|
||||
template<mutex::Delegate<T&> F>
|
||||
inline decltype(auto) with_lock(F&& f) & noexcept(std::is_nothrow_invocable_v<F, T&>)
|
||||
{
|
||||
std::lock_guard ul{ this->get_mutex() };
|
||||
return std::forward<F>(f)(this->value());
|
||||
}
|
||||
|
||||
template<mutex::Delegate<T const&> F>
|
||||
inline decltype(auto) with_lock(F&& f) const& noexcept(std::is_nothrow_invocable_v<F, T const&>)
|
||||
{
|
||||
std::shared_lock ul{ this->get_mutex() };
|
||||
return std::forward<F>(f)(this->value());
|
||||
}
|
||||
|
||||
template<mutex::Delegate<T&&> F>
|
||||
inline decltype(auto) with_lock(F&& f) && noexcept(std::is_nothrow_invocable_v<F, T&&>)
|
||||
{
|
||||
//std::shared_lock ul{ this->get_mutex() };
|
||||
return std::forward<F>(f)(std::move(this->value()));
|
||||
}
|
||||
|
||||
template<mutex::Delegate<T const&&> F>
|
||||
inline decltype(auto) with_lock(F&& f) const&& noexcept(std::is_nothrow_invocable_v<F, T const&&>)
|
||||
{
|
||||
std::shared_lock ul{ this->get_mutex() };
|
||||
return std::forward<F>(f)(std::move(this->value()));
|
||||
}
|
||||
|
||||
template<mutex::Delegate<T&> F>
|
||||
inline decltype(auto) with_unique_lock(F&& f) const noexcept(std::is_nothrow_invocable_v<F, T>)
|
||||
{
|
||||
std::unique_lock ul{ this->get_mutex() };
|
||||
if constexpr(std::is_invocable_v<F, T, decltype(ul)>) {
|
||||
return std::forward<F>(f)(*this->get_raw_ptr(), ul);
|
||||
} else return std::forward<F>(f)(*this->get_raw_ptr());
|
||||
}
|
||||
|
||||
template<mutex::Delegate<T const&> F>
|
||||
inline decltype(auto) with_shared_lock(F&& f) const noexcept(std::is_nothrow_invocable_v<F, T>)
|
||||
{
|
||||
std::shared_lock ul{ this->get_mutex() };
|
||||
if constexpr(std::is_invocable_v<F, T, decltype(ul)>) {
|
||||
return std::forward<F>(f)(*this->get_raw_ptr(), ul);
|
||||
} else return std::forward<F>(f)(*this->get_raw_ptr());
|
||||
}
|
||||
};
|
||||
}
|
||||
template<typename T>
|
||||
using SharedMutex = shared::Mutex<T>;
|
||||
#if 0
|
||||
template<typename T>
|
||||
class Mutex {
|
||||
using value_type = std::decay_t<T>;
|
||||
mutable value_type m_value;
|
||||
protected:
|
||||
std::mutex m_mutex{}; // TODO: Factor this out into template parameter to allow for `SharedMutex<T>` (or `shared::Mutex<T>`) e.g as partial-spec for / inheritance from `fx::mutex::Wrapped<std::{shared_,}mutex, T>`
|
||||
public:
|
||||
constexpr Mutex() noexcept requires(std::is_default_constructible_v<value_type>)
|
||||
: m_value{} {}
|
||||
constexpr Mutex(T&& value) noexcept
|
||||
: m_value{std::forward<T>(value)} {}
|
||||
|
||||
Mutex(const Mutex&) = delete;
|
||||
Mutex& operator=(
|
||||
|
||||
Mutex(Mutex&&) = default;
|
||||
Mutex
|
||||
};
|
||||
|
||||
namespace shared {
|
||||
template<typename T>
|
||||
#error class Mutex /* See above TODO about dual-impl */
|
||||
}
|
||||
template<typename T>
|
||||
using SharedMutex = shared::Mutex<T>;
|
||||
#endif
|
||||
}
|
@ -0,0 +1,251 @@
|
||||
//! Sync cond-var queue
|
||||
#pragma once
|
||||
|
||||
#include <span>
|
||||
|
||||
#include <queue>
|
||||
#include <optional>
|
||||
|
||||
#include "error.hh"
|
||||
#include "condvar.hh"
|
||||
|
||||
namespace fx {
|
||||
namespace details [[gnu::visibility("hidden")]] {
|
||||
// std::queue::pop() is VOID?????
|
||||
inline auto pop_queue(auto& q) {
|
||||
auto p = std::move(q.front());
|
||||
q.pop();
|
||||
return p;
|
||||
};
|
||||
}
|
||||
template<typename T, size_t N = std::dynamic_extent>
|
||||
struct Queue;
|
||||
|
||||
//TODO: Fixed-sized queue: template<typename T, size_t N> Queue<T, N> { ... };
|
||||
// TODO: In non-`dynamic_extent` Queue<T, N>, we can use `.wait_then(has_less_than_max_elements, st)` in `.push(T&&[, st])` to wait if queue has >= N elements already in (on optional stop-token `st`.).
|
||||
|
||||
template<typename T>
|
||||
class Queue<T, std::dynamic_extent> {
|
||||
template<typename, size_t N> //XXX: Uhh... Does this like, properly work to decl `Queue<T, any(N)>` as friend....?
|
||||
friend class Queue;
|
||||
|
||||
constexpr static inline auto has_element = [](auto const& q) noexcept { return !q.empty(); };
|
||||
public:
|
||||
using value_type = std::decay_t<T>;
|
||||
constexpr static inline size_t& extent = std::dynamic_extent;
|
||||
|
||||
constexpr static inline bool push_may_wait = false;
|
||||
|
||||
template<std::convertible_to<T> U = T>
|
||||
inline void push(U&& value)
|
||||
{
|
||||
m_queue.with_unique_lock([&](auto& queue) { queue.push(std::forward<U>(value)); });
|
||||
m_queue.notify_one();
|
||||
}
|
||||
inline value_type pop()
|
||||
{
|
||||
return this->m_queue.wait_then([] (auto& queue) { return details::pop_queue( queue ); }, has_element);
|
||||
}
|
||||
#if 1
|
||||
inline std::optional<value_type> try_pop(std::stop_token st) noexcept
|
||||
{
|
||||
std::optional<value_type> v{ std::nullopt };
|
||||
m_queue.try_wait_then([&v](auto& queue) {
|
||||
v.emplace( std::move(queue.front()) );
|
||||
queue.pop();
|
||||
}, has_element, st);
|
||||
return v;
|
||||
}
|
||||
[[gnu::nonnull]]
|
||||
inline value_type* try_pop(value_type* __restrict__ v, std::stop_token st) noexcept
|
||||
{
|
||||
if(not st.stop_possible()) {
|
||||
std::exchange(*v, this->pop());
|
||||
return true;
|
||||
}
|
||||
return m_queue.try_wait_then([v](auto& queue) {
|
||||
std::exchange(*v, queue.front());
|
||||
queue.pop();
|
||||
}, has_element, st)
|
||||
? v
|
||||
: nullptr;
|
||||
}
|
||||
#endif
|
||||
inline value_type pop(std::stop_token st)
|
||||
{
|
||||
#if 1
|
||||
return m_queue.wait_then([](auto& queue) { return details::pop_queue( queue ); }, has_element, st);
|
||||
#else
|
||||
// This is *really* ugly but XXX: we don't have a throwing `wait_then()`, so... TODO: Go add a `wait_then_or()` to SharedCondVar, which throws `OperationCancelledError` if wait() returns `false`, and then that can be a `decltype(auto)`, `return `fwd(f)(mut_value())` function like the rest.
|
||||
std::aligned_storage_t<sizeof(value_type), alignof(value_type)> store;
|
||||
value_type* v = static_cast<value_type*>(store.data);
|
||||
|
||||
if(! m_queue.try_wait_then([v](auto& queue) {
|
||||
std::construct_at(v, queue.pop());
|
||||
}, has_element, st))
|
||||
error::throw_cancelled();
|
||||
|
||||
// Destroy `*v` after return.
|
||||
struct defer_destroy {
|
||||
value_type* v;
|
||||
constexpr ~defer_destroy() {
|
||||
if constexpr(not std::is_trivially_destructible_v<value_type>) {
|
||||
std::destroy_at(v);
|
||||
}
|
||||
}
|
||||
} _ddest{v};
|
||||
|
||||
return std::move(*v);
|
||||
#endif
|
||||
}
|
||||
|
||||
inline size_t size() const noexcept { return m_queue.with_shared_lock([] (auto const& q) noexcept { return q.size(); }); }
|
||||
inline bool empty() const noexcept { return m_queue.with_shared_lock([] (auto const& q) noexcept { return q.empty(); }); }
|
||||
|
||||
[[gnu::const, gnu::always_inline, gnu::artificial]]
|
||||
constexpr virtual bool can_push() const noexcept { return true; } // Always true for dynamic_extent
|
||||
inline bool can_pop() const noexcept { return not empty(); } // If the queue is not empty, a pop can be requested by someone.
|
||||
|
||||
// Not-appliccable on non-max-sized (`N = std::dynamic_extent`) queues
|
||||
size_t left() const noexcept = delete;
|
||||
size_t capacity() const noexcept = delete;
|
||||
#if 0
|
||||
protected:
|
||||
#warning "XXX: `wait()` functions are **NOT SAFE**, they all consume a `push()`'s `notify_one()` **without** reducing queue pressure; they should not be used except as an internal interface that also pops the queue via some other method after the wait completes."
|
||||
|
||||
// Wait for an element to be pushed without popping it.
|
||||
//
|
||||
// # Returns
|
||||
// If there was an element and `st` was not stopped.
|
||||
// # Throws
|
||||
// `error::OperationCancelledError` - If `st` is fired while waiting.
|
||||
inline bool wait(std::stop_token st) const
|
||||
{
|
||||
return m_queue.wait_then_shared([](auto const& q) noexcept { return ! q.empty(); }, has_element, st);
|
||||
}
|
||||
|
||||
/// Same as `wait(st)` but returns `false` on cancellation instead of throwing.
|
||||
inline bool try_wait(std::stop_token st) const noexcept
|
||||
{
|
||||
bool elem = false;
|
||||
if(! m_queue.try_wait_then_shared([&](auto const& q) noexcept { elem = !q.empty(); }, has_element, st))
|
||||
return false;
|
||||
return elem;
|
||||
}
|
||||
|
||||
/// Waits for an element to be available without popping it.
|
||||
inline bool wait() const
|
||||
{
|
||||
return m_queue.wait_then_shared([](auto const& q) noexcept { return ! q.empty(); }, has_element);
|
||||
}
|
||||
#endif /* unsafe interface removed (see above) */
|
||||
private:
|
||||
shared::CondVar< std::queue<value_type> > m_queue{};
|
||||
};
|
||||
|
||||
template<typename T, size_t N>
|
||||
class Queue : private Queue<T> {
|
||||
constexpr static inline auto& has_element = Queue<T>::has_element;
|
||||
constexpr static inline auto has_less_than_max = [](auto const& q) noexcept { return q.size() <= N; };
|
||||
public:
|
||||
constexpr static inline size_t extent = N;
|
||||
|
||||
constexpr static inline bool push_may_wait = true;
|
||||
|
||||
using typename Queue<T>::value_type;
|
||||
using Queue<T>::Queue;
|
||||
|
||||
template<std::convertible_to<T> U = T>
|
||||
inline bool try_push(U&& value, std::stop_token st)
|
||||
{
|
||||
std::unique_lock plk { this->m_queue.get_mutex() }; //XXX: Is this okay? To keep the lock going...?
|
||||
while(! has_less_than_max(*this->m_queue.get_ptr())) {
|
||||
if(! m_send.wait(plk, st, [this] () noexcept { return has_less_than_max(this->m_queue.get_ptr()); }))
|
||||
return false;
|
||||
}
|
||||
|
||||
// We have a unique_lock, we have waited to be signalled if queue pressure not has_less_than_max(), it is okay to push and notify poppers now.
|
||||
this->m_queue.get_ptr_unsafe()->push(std::forward<U>(value));
|
||||
this->m_queue.notify_one();
|
||||
return true;
|
||||
}
|
||||
|
||||
template<std::convertible_to<T> U = T>
|
||||
inline void push(U&& value)
|
||||
{
|
||||
std::unique_lock plk { this->m_queue.get_mutex() }; //XXX: Is this okay? To keep the lock going...?
|
||||
while(! has_less_than_max(*this->m_queue.get_ptr()))
|
||||
m_send.wait(plk, [this] () noexcept { return has_less_than_max(this->m_queue.get_ptr()); });
|
||||
|
||||
// We have a unique_lock, we have waited to be signalled if queue pressure not has_less_than_max(), it is okay to push and notify poppers now.
|
||||
this->m_queue.get_ptr_unsafe()->push(std::forward<U>(value));
|
||||
this->m_queue.notify_one();
|
||||
}
|
||||
|
||||
template<std::convertible_to<T> U = T>
|
||||
inline void push(U&& value, std::stop_token st)
|
||||
{
|
||||
std::unique_lock plk { this->m_queue.get_mutex() }; //XXX: Is this okay? To keep the lock going...?
|
||||
while(! has_less_than_max(*this->m_queue.get_ptr())) {
|
||||
if(! m_send.wait(plk, st, [this] () noexcept { return has_less_than_max(this->m_queue.get_ptr()); }))
|
||||
error::throw_cancelled();
|
||||
}
|
||||
|
||||
// We have a unique_lock, we have waited to be signalled if queue pressure not has_less_than_max(), it is okay to push and notify poppers now.
|
||||
this->m_queue.get_ptr_unsafe()->push(std::forward<U>(value));
|
||||
this->m_queue.notify_one();
|
||||
}
|
||||
|
||||
inline value_type pop()
|
||||
{
|
||||
bool was_full = false;
|
||||
value_type v = this->m_queue.wait_then([&] (auto& queue) {
|
||||
was_full = not has_less_than_max(queue);
|
||||
return details::pop_queue( queue );
|
||||
}, has_element);
|
||||
// Notify a waiting pusher to re-check their condition.
|
||||
//XXX: Do we only want to do this if `not has_less_than_max()`?
|
||||
(void)was_full; //For now: The cond might be outdated, so notify regardless
|
||||
m_send.notify_one();
|
||||
return v;
|
||||
}
|
||||
|
||||
inline value_type pop(std::stop_token st)
|
||||
{
|
||||
bool was_full = false;
|
||||
value_type v = this->m_queue.wait_then([&] (auto& queue) {
|
||||
was_full = not has_less_than_max(queue);
|
||||
return details::pop_queue( queue );
|
||||
}, has_element, st);
|
||||
// Notify a waiting pusher to re-check their condition.
|
||||
//XXX: Do we only want to do this if `not has_less_than_max()`?
|
||||
(void)was_full; //For now: The cond might be outdated, so notify regardless
|
||||
m_send.notify_one();
|
||||
return v;
|
||||
}
|
||||
|
||||
//TODO: try_pop(std::stop_token st)
|
||||
|
||||
// Accessors (same as `Queue<T, std::dynamic_extent>`.)
|
||||
|
||||
using Queue<T>::size;
|
||||
using Queue<T>::empty;
|
||||
|
||||
// May be `false` in this queue.
|
||||
inline virtual bool can_push() const noexcept override final { return has_space(); }
|
||||
using Queue<T>::can_pop;
|
||||
|
||||
// Only appliccable on non-max-sized (`N = std::dynamic_extent`) queues
|
||||
inline size_t left() const noexcept { return extent - this->size(); }
|
||||
[[gnu::const, gnu::always_inline, gnu::artificial]]
|
||||
constexpr size_t capacity() const noexcept { return extent; }
|
||||
protected:
|
||||
inline bool has_space() const
|
||||
{
|
||||
return this->m_queue.with_shared_lock(has_less_than_max);
|
||||
}
|
||||
private:
|
||||
mutable std::condition_variable_any m_send{};
|
||||
};
|
||||
|
||||
}
|
@ -0,0 +1,321 @@
|
||||
//! Multi-threaded signal handling.
|
||||
#pragma once
|
||||
#include <utility>
|
||||
#include <thread>
|
||||
#include <functional> // needed for std::bind().... (TODO: How to remove use of bind...?)
|
||||
#include <array>
|
||||
|
||||
#include <signal.h> // Needed for `sigset_t`
|
||||
|
||||
namespace fx::signal {
|
||||
namespace details [[gnu::visibility("hidden")]] {
|
||||
|
||||
}
|
||||
|
||||
constexpr static inline struct wrap_callback_t {} wrap_callback {};
|
||||
constexpr static inline struct with_add_t {} add_signals {};
|
||||
|
||||
template<typename F>
|
||||
class wait_for_signals;
|
||||
|
||||
/// Create a function-object that, when invoked, blocks the thread until it receives one of the signals specified in construction.
|
||||
///
|
||||
/// On destruction, the signal mask is reset to what it was before the object was constructed (unless disabled entirely.)
|
||||
template<>
|
||||
class wait_for_signals<void> {
|
||||
template<typename> friend class wait_for_signals;
|
||||
|
||||
sigset_t oldset;
|
||||
sigset_t sigset = {0};
|
||||
|
||||
// Impl for add w/o arg-pack
|
||||
explicit wait_for_signals(std::initializer_list<int> sigs, with_add_t const&, std::initializer_list<int> add) noexcept;
|
||||
|
||||
[[gnu::artificial]]
|
||||
inline
|
||||
void _do_signal_reached(bool move, int signum) {
|
||||
if(move) std::move(*this)
|
||||
.do_signal_reached(signum);
|
||||
else do_signal_reached(signum);
|
||||
}
|
||||
|
||||
static int wait_for_signal(sigset_t const& sigset);
|
||||
protected:
|
||||
//inline int wait_for_signal() { return wait_for_signal(sigset); }
|
||||
|
||||
virtual void do_signal_reached(int signum) &;
|
||||
virtual void do_signal_reached(int signum) &&;
|
||||
|
||||
inline wait_for_signals(std::initializer_list<int> sigs, std::convertible_to<int> auto const&... add) noexcept
|
||||
requires(sizeof...(add) > 0)
|
||||
: wait_for_signals(sigs, add_signals, { int(add)... }) {}
|
||||
|
||||
virtual bool do_reset_mask() noexcept;
|
||||
|
||||
//virtual
|
||||
int operator()(sigset_t const& sigset) &;
|
||||
//virtual
|
||||
int operator()(sigset_t const& sigset) &&;
|
||||
|
||||
template<typename W>
|
||||
inline decltype(auto) operator()(wrap_callback_t const&, W&& wrapper, sigset_t const& sigset) &
|
||||
{
|
||||
int signum = wait_for_signal(sigset);
|
||||
|
||||
return std::forward<W>(wrapper)(std::bind(&wait_for_signals<void>::_do_signal_reached, this, false, std::placeholders::_1), signum);
|
||||
}
|
||||
|
||||
template<typename W>
|
||||
inline decltype(auto) operator()(wrap_callback_t const&, W&& wrapper, sigset_t const& sigset) &&
|
||||
{
|
||||
int signum = wait_for_signal(sigset);
|
||||
|
||||
return std::forward<W>(wrapper)(std::bind(&wait_for_signals<void>::_do_signal_reached, this, true, std::placeholders::_1), signum);
|
||||
}
|
||||
public:
|
||||
wait_for_signals(std::initializer_list<int> sigs) noexcept;
|
||||
|
||||
inline explicit wait_for_signals(std::convertible_to<int> auto const&... sigs) noexcept
|
||||
requires(sizeof...(sigs) > 0)
|
||||
: wait_for_signals({ int(sigs)... }) {}
|
||||
|
||||
wait_for_signals(wait_for_signals const&) = delete;
|
||||
wait_for_signals& operator=(wait_for_signals const&) = delete;
|
||||
|
||||
wait_for_signals(wait_for_signals&& m) noexcept;
|
||||
wait_for_signals& operator=(wait_for_signals&& m) noexcept = delete; // NOTE: We can't have multiple sigmasks so there's no reason for this to exist (XXX: Is it actually okay if the previous mask is reset first...? I don't know...)
|
||||
|
||||
/// Do not reset the signal mask on destruction.
|
||||
///
|
||||
/// This will leave the blocked signals handled by the invocation operator blocked and `sigwait()` can still be used on them.
|
||||
virtual void detach() && noexcept;
|
||||
|
||||
/// Reset the signal mask right now.
|
||||
///
|
||||
/// This also detaches the handle from doing it on destruction (see `detach()`.)
|
||||
bool reset_mask() noexcept;
|
||||
|
||||
virtual ~wait_for_signals() noexcept;
|
||||
|
||||
int operator()() &;
|
||||
int operator()() &&;
|
||||
|
||||
|
||||
/// W is a function that takes a function-object with the signature `void(int)&&` bound to `this`.
|
||||
template<typename W>
|
||||
inline decltype(auto) operator()(wrap_callback_t const& w, W&& wrapper) &&
|
||||
{
|
||||
return std::move(*this)(w, std::forward<W>(wrapper), this->sigset);
|
||||
}
|
||||
|
||||
/// W is a function that takes a function-object with the signature `void(int)&` bound to `this`.
|
||||
template<typename W>
|
||||
inline decltype(auto) operator()(wrap_callback_t const& w, W&& wrapper) &
|
||||
{
|
||||
return this->operator()<W>(w, std::forward<W>(wrapper), this->sigset);
|
||||
}
|
||||
};
|
||||
|
||||
//#error "TODO: XXX: Eh, this impl doesn't work... Why does previous `int operator()` retain precedence when we re-make them here...???"
|
||||
// XXX: Remember the virtual functions always polute the namespace for some fucking reason...
|
||||
|
||||
template<typename F>
|
||||
class wait_for_signals : private wait_for_signals<void> {
|
||||
using base_t = wait_for_signals<void>;
|
||||
F on_sig;
|
||||
|
||||
inline
|
||||
int do_wait_now(sigset_t const& sigset, bool move) noexcept
|
||||
{
|
||||
if(move) return static_cast<base_t &&>(*this)(sigset);
|
||||
else return static_cast<base_t & >(*this)(sigset);
|
||||
}
|
||||
inline
|
||||
int do_wait_now(bool move) noexcept
|
||||
{
|
||||
if(move) return static_cast<base_t &&>(*this)();
|
||||
else return static_cast<base_t & >(*this)();
|
||||
}
|
||||
protected:
|
||||
[[gnu::artificial]]
|
||||
inline virtual void do_signal_reached(int) & override {}
|
||||
[[gnu::artificial]]
|
||||
inline virtual void do_signal_reached(int) && override {}
|
||||
|
||||
using base_t::do_reset_mask;
|
||||
|
||||
template<std::convertible_to<F> U = F>
|
||||
inline wait_for_signals(U&& on_sig, std::initializer_list<int> sigs, std::convertible_to<int> auto const&... add) noexcept(std::is_nothrow_convertible_v<U, F>)
|
||||
requires(sizeof...(add) > 0)
|
||||
: base_t(sigs, std::forward<decltype(add)>(add)...)
|
||||
, on_sig(std::forward<U>(on_sig)) {}
|
||||
|
||||
|
||||
inline decltype(auto) operator()(sigset_t const& sigset) & noexcept(std::is_nothrow_invocable_v<F, const int&>)
|
||||
{
|
||||
return std::invoke(on_sig, do_wait_now(sigset, false));
|
||||
}
|
||||
inline decltype(auto) operator()(sigset_t const& sigset) && noexcept(std::is_nothrow_invocable_v<F, const int&>)
|
||||
{
|
||||
return std::invoke(std::move(on_sig), do_wait_now(sigset, true));
|
||||
}
|
||||
|
||||
template<typename W>
|
||||
requires(std::is_invocable_v<W, F&, int const&>)
|
||||
inline decltype(auto) operator()(wrap_callback_t const&, W&& wrapper, sigset_t const& sigset) & noexcept(std::is_nothrow_invocable_v<W, F&, const int&>)
|
||||
{
|
||||
return std::invoke(std::forward<W>(wrapper), on_sig, do_wait_now(sigset, false));
|
||||
}
|
||||
|
||||
template<typename W>
|
||||
requires(std::is_invocable_v<W, F&&, int const&>)
|
||||
inline decltype(auto) operator()(wrap_callback_t const&, W&& wrapper, sigset_t const& sigset) && noexcept(std::is_nothrow_invocable_v<W, F&&, const int&>)
|
||||
{
|
||||
return std::invoke(std::forward<W>(wrapper), std::move(on_sig), do_wait_now(sigset, true));
|
||||
}
|
||||
public:
|
||||
template<std::convertible_to<F> U = F>
|
||||
inline wait_for_signals(U&& on_sig, std::initializer_list<int> sigs) noexcept(std::is_nothrow_convertible_v<U, F>)
|
||||
requires(std::is_invocable_v<F, int const&>)
|
||||
: base_t(sigs)
|
||||
, on_sig(std::forward<U>(on_sig)){}
|
||||
template<std::convertible_to<F> U = F>
|
||||
inline wait_for_signals(U&& on_sig, std::convertible_to<int> auto const&... sigs) noexcept(std::is_nothrow_convertible_v<U, F>)
|
||||
requires(sizeof...(sigs) > 0)
|
||||
: wait_for_signals(std::forward<U>(on_sig), { int(sigs)... }) {}
|
||||
|
||||
wait_for_signals(wait_for_signals const&) = delete;
|
||||
wait_for_signals& operator=(wait_for_signals const&) = delete;
|
||||
|
||||
wait_for_signals(wait_for_signals&& m) noexcept(std::is_nothrow_move_constructible_v<F>) requires(std::is_move_constructible_v<F>) = default;
|
||||
wait_for_signals& operator=(wait_for_signals&&) = delete; //TODO: Once <void> spec has been complete as baseclassi (XXX: **AND** we decide to make this implementable... see base for reason why not.), this can be defaulted instead.
|
||||
|
||||
|
||||
inline decltype(auto) operator()() & noexcept(std::is_nothrow_invocable_v<F, const int&>)
|
||||
{
|
||||
return std::invoke(on_sig, do_wait_now(false));
|
||||
}
|
||||
inline decltype(auto) operator()() && noexcept(std::is_nothrow_invocable_v<F, const int&>)
|
||||
{
|
||||
return std::invoke(std::move(on_sig), do_wait_now(true));
|
||||
}
|
||||
|
||||
using base_t::detach;
|
||||
using base_t::reset_mask;
|
||||
|
||||
template<typename W>
|
||||
requires(std::is_invocable_v<W, F&, int const&>)
|
||||
inline decltype(auto) operator()(wrap_callback_t const&, W&& wrapper) & noexcept(std::is_nothrow_invocable_v<W, F&, const int&>)
|
||||
{
|
||||
return std::invoke(std::forward<W>(wrapper), on_sig, do_wait_now(false));
|
||||
}
|
||||
|
||||
template<typename W>
|
||||
requires(std::is_invocable_v<W, F&&, int const&>)
|
||||
inline decltype(auto) operator()(wrap_callback_t const&, W&& wrapper) && noexcept(std::is_nothrow_invocable_v<W, F&&, const int&>)
|
||||
{
|
||||
return std::invoke(std::forward<W>(wrapper), std::move(on_sig), do_wait_now(true));
|
||||
}
|
||||
|
||||
virtual ~wait_for_signals() noexcept = default;
|
||||
};
|
||||
|
||||
|
||||
template<typename R, typename... Args>
|
||||
struct wait_for_signals< R (Args...) > : public wait_for_signals< std::function< R(Args...) > > {
|
||||
using wait_for_signals< std::function< R(Args...) > >::wait_for_signals;
|
||||
|
||||
using wait_for_signals< std::function< R(Args...) > >::operator();
|
||||
|
||||
virtual ~wait_for_signals() noexcept = default;
|
||||
};
|
||||
|
||||
|
||||
extern template class wait_for_signals<void>;
|
||||
extern template class wait_for_signals<void(*)(int)>;
|
||||
extern template class wait_for_signals< std::function< void(int) > >;
|
||||
|
||||
template<typename F>
|
||||
requires(std::is_invocable_v<F, int const&> or std::is_invocable_v<F, int const&, std::stop_token>)
|
||||
class thread_wait_for_signals : wait_for_signals< F > {
|
||||
using base_t = wait_for_signals<F>;
|
||||
constexpr static inline bool TAKES_TOKEN = std::is_invocable_v<F, int const&, std::stop_token>;
|
||||
|
||||
int m_exit_signal;
|
||||
std::jthread m_thread{};
|
||||
public:
|
||||
//TODO: Port this...
|
||||
inline int exit_signal() const noexcept { return m_exit_signal; }
|
||||
|
||||
inline std::jthread& thread_handle() noexcept { return m_thread; }
|
||||
inline std::jthread const& thread_handle() const noexcept { return m_thread; }
|
||||
|
||||
template<std::convertible_to<F> U = F>
|
||||
inline thread_wait_for_signals(U&& on_sig, std::initializer_list<int> sigs, int exit = SIGTERM) noexcept(std::is_nothrow_convertible_v<U, F>)
|
||||
: base_t(std::forward<U>(on_sig), sigs, exit)
|
||||
, m_exit_signal(exit)
|
||||
{
|
||||
m_thread = std::jthread([this](std::stop_token st) { // XXX: This means the handle **must be pinned** and cannot be moved, see below.
|
||||
if constexpr(TAKES_TOKEN) {
|
||||
std::move(*this)(wrap_callback, [st] (F&& func, int const& signum) noexcept(std::is_nothrow_invocable_v<F, int const&>) -> decltype(auto) {
|
||||
return std::invoke(std::forward<F>(func), signum, st);
|
||||
});
|
||||
} else {
|
||||
std::move(*this)();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
[[gnu::returns_nonnull, gnu::pure]]
|
||||
inline std::jthread* operator->() noexcept { return &thread_handle(); }
|
||||
[[gnu::returns_nonnull, gnu::pure]]
|
||||
inline std::jthread const* operator->() const noexcept { return &thread_handle(); }
|
||||
|
||||
thread_wait_for_signals(thread_wait_for_signals&&) = delete; // = default; XXX: This is wrong! Sigset is not captured by value in the thread callback, it refers to `base::sigset&`. In order to make this move allowed, we must expose sigset **and** oldset to the ctor... Another option would be capturing *this as `base_t` by *move* *in* the constructor itself, but idk if that is legal tbh... It'd be better to just copy over the signal sets and give them to the callback before invoking it somehow i think... (XXX: This is actually not possible as the thread ctor also captures ref-access to the callback function as well, so...
|
||||
|
||||
// Prevent the exit-wake from happening.
|
||||
//
|
||||
// The thread handle is returned. To detach the handle entirely, use `std::move(self).release().detach()`.
|
||||
// NOTE: Once this is called, to wake-kill the thread, use `pthread_kill(native_handle(), self.exit_signal())`.
|
||||
// NOTE: The signal mask is **NOT** reset when this is called, you must also call `.reset_mask()` after the thread completes.
|
||||
[[nodiscard]]
|
||||
inline std::jthread release() && noexcept {
|
||||
return std::move(m_thread);
|
||||
}
|
||||
|
||||
|
||||
// Both detach the thread from the handle and prevent the signal mask from being reset.
|
||||
inline virtual void detach() && noexcept override {
|
||||
// Detach the thread management.
|
||||
m_thread.detach();
|
||||
// Ensure the sigmask-reset is disabled *as well*.
|
||||
static_cast<base_t&&>(*this).detach();
|
||||
}
|
||||
|
||||
inline virtual ~thread_wait_for_signals() noexcept {
|
||||
m_thread.request_stop();
|
||||
|
||||
//TODO: Instead, we could have the internal spawned thread issue a stop_callback that calls `raise(SIGTERM)`?
|
||||
// That might make the whole handle-move-semantic thingy easier? But would also force the wake on any and all stop request, which... (XXX: That might be more desireable behaviour tbh...)
|
||||
if(m_thread.joinable()) {
|
||||
pthread_kill(m_thread.native_handle(), m_exit_signal);
|
||||
m_thread.join();
|
||||
}
|
||||
|
||||
// Parent ctor (resets signal mask.)
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
extern template class thread_wait_for_signals< void(*)(int) >;
|
||||
extern template class thread_wait_for_signals< void(*)(int, std::stop_token) >;
|
||||
|
||||
extern template class thread_wait_for_signals<std::function<void(int)> >;
|
||||
extern template class thread_wait_for_signals<std::move_only_function<void(int)> >;
|
||||
|
||||
extern template class thread_wait_for_signals<std::function<void(int, std::stop_token)> >;
|
||||
extern template class thread_wait_for_signals<std::move_only_function<void(int, std::stop_token)> >;
|
||||
|
||||
template<typename F>
|
||||
thread_wait_for_signals(F&& func, std::initializer_list<int>, int) -> thread_wait_for_signals<F>;
|
||||
}
|
@ -0,0 +1,144 @@
|
||||
#include <array>
|
||||
|
||||
#include <error.hh>
|
||||
#include <signal.hh>
|
||||
|
||||
#include "macros.h"
|
||||
|
||||
namespace fx::signal {
|
||||
namespace {
|
||||
template<typename T>
|
||||
requires(std::is_trivial_v<T>)
|
||||
constexpr static bool biteq(T a, T b) noexcept
|
||||
{
|
||||
typedef std::array<unsigned char, sizeof(T)> A alignas(T);
|
||||
__builtin_clear_padding(&a);
|
||||
__builtin_clear_padding(&b);
|
||||
|
||||
return std::bit_cast<A>(a) == std::bit_cast<A>(b);
|
||||
}
|
||||
}
|
||||
|
||||
#undef IMPL_SPEC
|
||||
#define IMPL_CLASS wait_for_signals
|
||||
#define IMPL_SPEC <void>
|
||||
$ctor(std::initializer_list<int> sigs, with_add_t const&, std::initializer_list<int> add) noexcept
|
||||
{
|
||||
sigset_t cur_set = {0};
|
||||
sigset_t add_set = {0};
|
||||
|
||||
sigemptyset(&cur_set);
|
||||
for(const int sig : sigs)
|
||||
sigaddset(&cur_set, sig);
|
||||
|
||||
sigemptyset(&add_set);
|
||||
for(const int sig : add)
|
||||
sigaddset(&add_set, sig);
|
||||
|
||||
sigemptyset(&sigset);
|
||||
sigorset(&sigset, &cur_set, &add_set);
|
||||
|
||||
|
||||
pthread_sigmask(SIG_BLOCK, &sigset, &oldset);
|
||||
}
|
||||
|
||||
int $ wait_for_signal(sigset_t const& sigset)
|
||||
{
|
||||
int signum = 0;
|
||||
if(__builtin_expect(sigwait(&sigset, &signum) != 0, false))
|
||||
fx::error::throw_last_system_error();
|
||||
return signum;
|
||||
}
|
||||
|
||||
void $ do_signal_reached(int signum) & { (void)signum; }
|
||||
void $ do_signal_reached(int signum) && { (void)signum; }
|
||||
|
||||
bool $ do_reset_mask() noexcept
|
||||
{
|
||||
// If the oldset is not empty (i.e. object has not been moved,) then we re-set the mask to the oldset IFF `sigset` is also not empty. If it is, we do not know if the object *was* move, but *nothing will have changed* in the mask, so no need to re-set the mask anyway.
|
||||
if((not sigisemptyset(&oldset)) || __builtin_expect(! biteq(oldset, sigset), true))
|
||||
// XXX: I'm not sure if this ^ check is actually what we want wrt. moving, but for non-moved handles, it seems to work fine. (See thread-spawning version for issues.)
|
||||
pthread_sigmask(SIG_SETMASK, &oldset, &sigset);
|
||||
else return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
int $ operator()(sigset_t const& sigset) &
|
||||
{
|
||||
int signum = wait_for_signal(sigset);
|
||||
|
||||
do_signal_reached(signum);
|
||||
return signum;
|
||||
}
|
||||
int $ operator()(sigset_t const& sigset) &&
|
||||
{
|
||||
int signum = wait_for_signal(sigset);
|
||||
|
||||
std::move(*this).do_signal_reached(signum);
|
||||
return signum;
|
||||
}
|
||||
|
||||
|
||||
$ctor(std::initializer_list<int> sigs) noexcept
|
||||
{
|
||||
sigemptyset(&sigset);
|
||||
for(const int sig : sigs)
|
||||
sigaddset(&sigset, sig);
|
||||
pthread_sigmask(SIG_BLOCK, &sigset, &oldset);
|
||||
}
|
||||
|
||||
$ctor_move(m) noexcept
|
||||
: oldset(m.oldset)
|
||||
, sigset(m.sigset) {
|
||||
// Ensure `m.oldset` is emptied so dtor doesn't try to unblock it and knows it's been moved.
|
||||
sigemptyset(&m.oldset);
|
||||
}
|
||||
|
||||
/// Do not reset the signal mask on destruction.
|
||||
///
|
||||
/// This will leave the blocked signals handled by the invocation operator blocked and `sigwait()` can still be used on them.
|
||||
void $ detach() && noexcept {
|
||||
sigemptyset(&oldset);
|
||||
}
|
||||
|
||||
/// Reset the signal mask right now.
|
||||
///
|
||||
/// This also detaches the handle from doing it on destruction (see `detach()`.)
|
||||
bool $ reset_mask() noexcept {
|
||||
bool v = do_reset_mask();
|
||||
std::move(*this).detach();
|
||||
return v;
|
||||
}
|
||||
|
||||
$dtor() noexcept
|
||||
{
|
||||
do_reset_mask();
|
||||
}
|
||||
|
||||
int $ operator()() &
|
||||
{ return this->operator()(this->sigset); }
|
||||
int $ operator()() &&
|
||||
{ return std::move(*this)(this->sigset); }
|
||||
|
||||
#undef IMPL_SPEC // template<> <void>
|
||||
|
||||
template class $class<void>;
|
||||
template class $class<void(*)(int)>;
|
||||
template class $class< std::function< void(int) > >;
|
||||
|
||||
#undef IMPL_CLASS // wait_for_signals
|
||||
#define IMPL_SPEC //
|
||||
|
||||
#define IMPL_CLASS thread_wait_for_signals
|
||||
extern template class $class< void(*)(int) >;
|
||||
extern template class $class< void(*)(int, std::stop_token) >;
|
||||
|
||||
extern template class $class<std::function<void(int)> >;
|
||||
extern template class $class<std::move_only_function<void(int)> >;
|
||||
|
||||
extern template class $class<std::function<void(int, std::stop_token)> >;
|
||||
extern template class $class<std::move_only_function<void(int, std::stop_token)> >;
|
||||
#undef IMPL_CLASS /* thread_wait_for_signals */
|
||||
|
||||
}
|
||||
|
@ -0,0 +1,40 @@
|
||||
#include <bit>
|
||||
#include <system_error>
|
||||
|
||||
#include <errno.h>
|
||||
|
||||
#include <mutex.hh>
|
||||
#include <condvar.hh>
|
||||
#include <queue.hh>
|
||||
|
||||
//#include <signal.hh> // NOTE: Moved to `fx-sig.cpp`
|
||||
|
||||
#include "macros.h"
|
||||
|
||||
namespace fx::error {
|
||||
OperationCancelledError::OperationCancelledError() noexcept
|
||||
: std::runtime_error("Operation cancelled") {}
|
||||
TaskCancelledError::TaskCancelledError() noexcept
|
||||
: std::runtime_error("Operation cancelled")
|
||||
, OperationCancelledError() {}
|
||||
|
||||
OperationCancelledError::~OperationCancelledError() = default;
|
||||
TaskCancelledError::~TaskCancelledError() = default;
|
||||
|
||||
[[gnu::noinline, gnu::cold, noreturn]]
|
||||
void throw_cancelled(bool clean)
|
||||
{
|
||||
if(clean) throw TaskCancelledError();
|
||||
else throw OperationCancelledError();
|
||||
}
|
||||
|
||||
void throw_if_cancelled(std::stop_token const& st, bool clean) {
|
||||
if(__builtin_expect(st.stop_requested(), false))
|
||||
throw_cancelled(clean);
|
||||
}
|
||||
|
||||
[[gnu::cold, noreturn]]
|
||||
void throw_last_system_error() {
|
||||
throw std::system_error{ int(errno), std::generic_category() };
|
||||
}
|
||||
}
|
@ -0,0 +1,80 @@
|
||||
#ifndef _OTPS_IMPL_MACROS_H
|
||||
#define _OTPS_IMPL_MACROS_H
|
||||
|
||||
#ifdef __cplusplus /* C++ only */
|
||||
extern "C" {
|
||||
|
||||
#define namespace$static namespace
|
||||
#define namespace$static$IN(N) namespace N { namespace
|
||||
|
||||
#define BLOCK$static(N) namespace$static$AT(N) {
|
||||
#define END$static } }
|
||||
|
||||
#define namespace$static$inline(V) namespace$static [[gnu::__visibility__( #V )]]
|
||||
#define BLOCK$static$inline(V) namespace$static$inline(V) {
|
||||
#define END$static$inline }
|
||||
|
||||
#define namespace$extern extern "C"
|
||||
#define BLOCK$extern namespace$extern {
|
||||
#define END$extern }
|
||||
|
||||
#define throw$error(X, ...) [] [[noreturn, gnu::noinline, gnu::cold]] (auto&&... ctor) static { throw ::std::decay_t<__typeof__(X)> { ::std::forward<decltype(ctor)>(ctor)... }; } \
|
||||
(__VA_ARGS__)
|
||||
|
||||
#else /* C only */
|
||||
|
||||
//NOTE: No replacement for `namespace$static[$inline]`: It cannot be affected in C syntax (without `#pragma GCC`.)
|
||||
|
||||
#define namespace$extern /* default behaviour in C */
|
||||
#define BLOCK$extern
|
||||
#define END$extern
|
||||
|
||||
#endif /* C & C++ */
|
||||
|
||||
#define throw$extern(_message) throw_last_system_error()
|
||||
extern void throw_last_system_error() __attribute__((__visibility__("internal"), __noinline__, __cold__, noreturn));
|
||||
|
||||
#define ASSUME(X) do { if(!(X)) __builtin_unreachable(); } while(0)
|
||||
|
||||
#define EXPECT(X, V) __builtin_expect(!!(X), !!(V))
|
||||
|
||||
#define LIKELY(X) EXPECT((X), 1)
|
||||
#define UNLIKELY(X) EXPECT((X), 0)
|
||||
|
||||
#define ASSUME_NONNULL(p) ((p) ?: __builtin_unreachable())
|
||||
#define ASSERT_NONNULL(p) ((p) ?: __builtin_trap())
|
||||
|
||||
//#define IMPL_CLASS N
|
||||
#define IMPL_SPEC $NONE /*<T>*/
|
||||
|
||||
#define $class IMPL_CLASS
|
||||
|
||||
#define $ $class IMPL_SPEC ::
|
||||
|
||||
#define $ctor $ $class
|
||||
#define $dtor $ ~ $class
|
||||
|
||||
#define $ctor_move(N) $ctor($class && N)
|
||||
#define $ctor_copy(N) $ctor($class const& N)
|
||||
|
||||
#define $assign $class & $ operator=
|
||||
|
||||
#define $assign_move(N) $assign($class && N)
|
||||
#define $assign_copy(N) $assign($class const& N)
|
||||
|
||||
#define this$not(N) (__builtin_expect(this != ::std::addressof(N), true))
|
||||
|
||||
/// OOL-defaults a class's destructor. (Useful for un-`inline`ing non-`constexpr` virtual dtors.
|
||||
///
|
||||
/// Doing this keeps the vtable definition within this TU, and reduces linker's redundency strain of generating multiple vtables.
|
||||
///
|
||||
/// Also since this TU has access to all non-`inline`, non-`constexpr` types and functions used within that class, the compiler can optimise more around the vtable and what happens in the destructor (de-virtualisation and pre-LTO inlining.)
|
||||
#define class$default(X, ...) X::~X() __VA_ARGS__ = default
|
||||
|
||||
//#undef IMPL_CLASS
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif /* _OTPS_IMPL_MACROS_H */
|
@ -0,0 +1,298 @@
|
||||
#include <condvar.hh>
|
||||
#include <queue.hh>
|
||||
#include <signal.hh>
|
||||
|
||||
#if 1 // FEATURE_FX_TEST
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include <latch>
|
||||
#include <atomic>
|
||||
#include <functional>
|
||||
#include <chrono>
|
||||
#include <bit>
|
||||
#include <array>
|
||||
|
||||
#include <cmath>
|
||||
#include <cstdio>
|
||||
|
||||
#include <signal.h>
|
||||
|
||||
namespace fx::test {
|
||||
//#warning "fx::Queue<T> Runtime test"
|
||||
|
||||
constexpr static auto factor = [](auto v) noexcept {
|
||||
std::remove_cvref_t<decltype(v)> total = 0;
|
||||
|
||||
while(v --> 0) total += v;
|
||||
return total;
|
||||
};
|
||||
|
||||
using namespace fx::signal;
|
||||
|
||||
template<typename F>
|
||||
requires(std::is_invocable_v<F>)
|
||||
class defer {
|
||||
F m_deferred;
|
||||
|
||||
public:
|
||||
template<std::convertible_to<F> U = F>
|
||||
constexpr defer(U&& f) noexcept(std::is_nothrow_convertible_v<U, F>)
|
||||
: m_deferred(std::forward<U>(f)) {}
|
||||
|
||||
defer(defer&&) noexcept = delete;
|
||||
defer(defer const&) = delete;
|
||||
|
||||
constexpr ~defer() noexcept(std::is_nothrow_invocable_v<F>) {
|
||||
std::invoke(std::move(m_deferred));
|
||||
}
|
||||
};
|
||||
|
||||
template<typename F>
|
||||
defer(F&&) -> defer<F>;
|
||||
|
||||
/// Blocks the specified signals on the current thread and any newly created ones, but the callback which blocks until any of them are received.
|
||||
/// After blocking the signals, this function creates and returns a callback which, when invoked, will block for any of the specified signals to arrive, and then invoke the callback passed `on_sig` with the number of the signal that arrived.
|
||||
///
|
||||
/// The returned callback captures `on_sig` via forwarding-decay, and the returned callback can be invoked any number of times as long as the captured `F` can be invoked more than once safely through a (non-`const`) reference.
|
||||
template<typename F>
|
||||
requires(std::is_invocable_v<F, int const&>)
|
||||
static auto make_wait_for_signals(F&& on_sig, std::initializer_list<int> sigs)
|
||||
{
|
||||
//auto sigset = std::make_unique_for_overwrite<sigset_t>(); // Should we actually need to have this a unique_ptr<sigset_t> and move it into return so address remains pinned...? I don't know if that is valuable or needed here tbh. (NOTE: Nope. Not needed.)
|
||||
sigset_t sigset = {0};
|
||||
sigemptyset(&sigset);
|
||||
for(const int sig : sigs)
|
||||
sigaddset(&sigset, sig);
|
||||
pthread_sigmask(SIG_BLOCK, &sigset, nullptr);
|
||||
|
||||
return [sigset, on_sig = std::forward<F>(on_sig)] () mutable noexcept(std::is_nothrow_invocable_v<F, int const&>) -> decltype(auto) {
|
||||
int signum = 0;
|
||||
if(__builtin_expect(sigwait(&sigset, &signum) != 0, false)) {
|
||||
if constexpr(std::is_nothrow_invocable_v<F, int const&>) {
|
||||
__builtin_trap();
|
||||
} else fx::error::throw_last_system_error();
|
||||
}
|
||||
return std::invoke(on_sig, signum);
|
||||
};
|
||||
}
|
||||
|
||||
void _so_sqrtx()
|
||||
{
|
||||
fx::Queue<int, 5> qu;
|
||||
|
||||
auto stop_workers = std::make_shared<std::stop_source>();
|
||||
std::vector<std::jthread> workers;
|
||||
|
||||
constexpr int WORKERS = 10;
|
||||
|
||||
//TODO: Impl this test.
|
||||
}
|
||||
|
||||
template<int N_WORKERS = 100>
|
||||
void _so_qrtx()
|
||||
{
|
||||
fx::Queue<int> qu;
|
||||
|
||||
auto stop_workers = std::make_shared<std::stop_source>();
|
||||
|
||||
|
||||
//auto&& rt_signals_callback
|
||||
#ifndef USE_STOP_LIFETIME
|
||||
/// If to couple rt_signals' lifetime to stop_workers' lifetime. (NOTE: See rest of comment on why this is not prefferential.)
|
||||
///
|
||||
/// If set to 1, we move stop_workers *below* rt_signals, so that it is destroyed *before* the signal thread handle asks it to stop.
|
||||
/// This results in the signal worker seeing stop_workers having been deallocated already, and assumes that is because the workers have already all stopped (which is true, but not the actual reason that happens here.)
|
||||
///
|
||||
/// If set to 0, we use the internal signal thread handle cancellation mechanism to check if this is a genuine signal or the "stop" control signal the handle sends when it goes out of scope. This results in the signal worker seeing it is being cancelled because *its own* scope has ran out, and not because the workers' scope has, and it produces a different message about being cancelled.
|
||||
///
|
||||
/// The former (1) ties the cancellation to the lifetime of the workers' cancellation source, which **MUST** be *move()*d to a new handle *below* the signal worker handle (which itself can be moved, so that must be tracked as well,) and is thus not advisable.
|
||||
///
|
||||
/// The latter (0) decouples the cancellation from the workers' cancellation source, and from everything else, making the cancellation check entirely internal to the handle, which can then be freely moved without lifetime concerns.
|
||||
# define USE_STOP_LIFETIME 0
|
||||
#endif
|
||||
|
||||
thread_wait_for_signals rt_signals( [stop_workers = std::weak_ptr(stop_workers)] (int sig
|
||||
#if ! USE_STOP_LIFETIME
|
||||
, std::stop_token st
|
||||
#endif
|
||||
) noexcept {
|
||||
|
||||
//std::stop_callback _dealloc_on_signals_exit(st, [&] () noexcept {
|
||||
// stop_workers.reset();
|
||||
//});
|
||||
|
||||
// NOTE: We **used to** also wait on `SIGTERM`, which allows us to send a "clean-up resources" signal; but now we reuse SIGINT for those purposes (with access to containing std::jthread's std::stop_token-based cancellation; or alternatively by ensuring that `stop_workers` has died before `rt_signals` does. The former is better for reasons outlined in the comment above.)
|
||||
#define fprintf(...) (sig != SIGTERM && fprintf(__VA_ARGS__), (void)0)
|
||||
if(sig == SIGINT /*|| sig == SIGTERM*/) {
|
||||
fprintf(stderr, " !!! requesting stop on SIGINT ...");
|
||||
#if ! USE_STOP_LIFETIME
|
||||
if(st.stop_requested())
|
||||
fprintf(stderr, " not stopping workers, operation cancelled by handle destruction; ignoring !!!\n");
|
||||
else
|
||||
#endif
|
||||
if(auto s = stop_workers.lock()) {
|
||||
fprintf(stderr, " [[[ stopping workers ]]] !!!\n");
|
||||
s->request_stop();
|
||||
} else fprintf(stderr, " workers already exited completely, ignoring & exiting !!!\n");
|
||||
} else fprintf(stderr, " !!! unknown signal received @ %-2d, ignoring !!!\n", sig);
|
||||
#undef fprintf
|
||||
}, {SIGINT}, SIGINT);
|
||||
|
||||
#if USE_STOP_LIFETIME
|
||||
// Since shadowing isn't supported in sepples for some reason (neither is gnucc's autotype,) use this stupid hack lol.
|
||||
auto $stop_workers = std::move(stop_workers);
|
||||
#define stop_workers $stop_workers
|
||||
#endif
|
||||
|
||||
std::vector<std::jthread> workers;
|
||||
|
||||
static_assert(N_WORKERS > 0 && N_WORKERS <= INT_MAX, "Invalid requested number of worker threads.");
|
||||
static_assert(factor( static_cast<signed __int128>(N_WORKERS) ) <= ((signed __int128)INT_MAX), "Requested number of worker threads too large for result to not overflow.");
|
||||
static_assert(static_cast<unsigned __int128>(N_WORKERS) <= static_cast<unsigned __int128>(std::latch::max()), "More threads than latch can take.");
|
||||
|
||||
using namespace std::chrono_literals;
|
||||
|
||||
constexpr static int WORKERS = int(N_WORKERS);
|
||||
constexpr static auto MAX_WAIT = 5s;
|
||||
constexpr static double WAIT_JITTER = 1.5;
|
||||
|
||||
std::latch start(3); // We wait for collector (below,) timeout control, & for main.
|
||||
//std::latch stop(WORKERS); // We wait for workers (below) to cancel collector.
|
||||
|
||||
// Set `stop_workers`' token to stop all the workers.
|
||||
std::stop_callback _stop_workers(stop_workers->get_token(), [&] {
|
||||
for(auto& w : workers) w.request_stop();
|
||||
});
|
||||
|
||||
fx::shared::CondVar<bool> worker_timeout(true);
|
||||
|
||||
std::jthread timeout_ctrl([&worker_timeout, &start] (std::stop_token st) noexcept {
|
||||
// XXX: Is taking the unique lock *here* but not releasing it until timeout below bad...? It will stop the other timeouts from being able to start, won't it? Idk, maybe not, cause we want *this* timeout to start first, so...
|
||||
std::unique_lock ulk{ worker_timeout.get_mutex() };
|
||||
start.arrive_and_wait();
|
||||
|
||||
fprintf(stderr, "[worker n/a] timeout max starting...\n");
|
||||
|
||||
if(not worker_timeout.get_condvar().wait_for(ulk, st, MAX_WAIT,
|
||||
[waiting = worker_timeout.get_ptr()]() noexcept { return ! *(volatile const bool*)waiting; }))
|
||||
{
|
||||
if(st.stop_requested()) {
|
||||
// NOTE: This behaviour is what we want, because if this thread has been cancelled, the workers will likely be cancelled by the same mechanism. In any canse, only stopping the cv update & notify when *this* thread is cancelled is what we want to do.
|
||||
fprintf(stderr, "[worker n/a] max timeout ctrl thread cancelled. Will not force end to worker timeouts.\n");
|
||||
return;
|
||||
}
|
||||
}
|
||||
// Unique lock is now active again, so we can change the value.
|
||||
*worker_timeout.get_ptr_unsafe() = false;
|
||||
|
||||
fprintf(stderr, "[worker n/a] max timeout reachaed, shephearding all workers to ignore rest of their timeouts...\n");
|
||||
|
||||
ulk.unlock();
|
||||
// Tell *all* the worker timeouts to refresh timeouts now & stop waiting.
|
||||
// It is safe to reuse this CV, because nothing else will notify this thread to stop its timeout other than an explicit st cancel.
|
||||
worker_timeout.notify_all();
|
||||
});
|
||||
|
||||
for(int i=0;i<WORKERS;i++) {
|
||||
std::jthread worker{[=, &qu, &start, &worker_timeout] (std::stop_token st) {
|
||||
start.wait(); // Wait for collector thread & main to signal start.
|
||||
|
||||
auto wait_time = std::sin(double(i) / double(WORKERS));
|
||||
|
||||
if(st.stop_requested()) return;
|
||||
|
||||
std::shared_lock shl{ worker_timeout.get_mutex() };
|
||||
if(! worker_timeout.get_condvar().wait_for(shl, st, (wait_time * WAIT_JITTER) * MAX_WAIT, [&worker_timeout]() noexcept -> bool {
|
||||
return ! *(volatile const bool*)worker_timeout.get_ptr();
|
||||
})) {
|
||||
|
||||
//fprintf(stderr, "[worker %-2d] timeout reached okay.\n", i);
|
||||
if(st.stop_requested()) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
else if(st.stop_requested()) {
|
||||
fprintf(stderr, "[worker %-2d] timeout broken by stop request, exiting before push.\n", i);
|
||||
return;
|
||||
}
|
||||
else fprintf(stderr, "[worker %-2d] timeout broken by global max-timeout-reached.\n", i);
|
||||
|
||||
qu.push(i);
|
||||
}};
|
||||
|
||||
workers.push_back(std::move(worker));
|
||||
}
|
||||
|
||||
std::atomic<int> total = 0;
|
||||
std::atomic<int> n=0, c=0;
|
||||
std::jthread collector([&] (std::stop_token st) {
|
||||
//NOTE: Not useful, also UB: we always join all workers (by **move**) before explicitly cancelling this thread.
|
||||
//std::stop_callback cancel_workers(st, [&] {
|
||||
// for(auto& v : workers) v.request_stop();
|
||||
//});
|
||||
|
||||
// Tell workers that collector thread has arrived.
|
||||
start.arrive_and_wait();
|
||||
|
||||
while(true) {
|
||||
int cur;
|
||||
printf("Collection %02d: ", cur = n++);
|
||||
try {
|
||||
int i = qu.pop(st);
|
||||
total += i;
|
||||
printf("%-2d (%02d)\n", i, c++);
|
||||
} catch(fx::error::OperationCancelledError const&) {
|
||||
printf("Cancelled (on op %d)!\n", cur);
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
std::stop_callback _stop_timeout_on_col_exit(stop_workers->get_token(), [&timeout_ctrl] () noexcept {
|
||||
// When signal handler is requested to stop, also stop the max timeout control thread.
|
||||
timeout_ctrl.request_stop();
|
||||
});
|
||||
|
||||
printf("-- Starting %d... --\n", WORKERS);
|
||||
start.count_down(); // Tell workers & collector thread to start.
|
||||
|
||||
// Wait for all workers to complete (NOTE: Not moved, so we can still refer to their state while main is joining.)
|
||||
for(auto& w : workers) {
|
||||
w.join();
|
||||
|
||||
// If the queue has pressure, check it.
|
||||
if(size_t len = qu.size()) {
|
||||
fprintf(stderr, " [ ..worker finished @ %-2lu.. ] ", len);
|
||||
}
|
||||
}
|
||||
fprintf(stderr, " << join all workers complete >>\n");
|
||||
|
||||
// Cancel the collector.
|
||||
collector.request_stop();
|
||||
|
||||
// Wait for it to exit.
|
||||
collector.join();
|
||||
printf("-- Complete --\n");
|
||||
bool ok; {
|
||||
constexpr auto expected = factor(WORKERS);
|
||||
auto ttl = total.load();
|
||||
printf("Total: %d (expected %d,) diff: %d.", ttl, expected, expected - ttl);
|
||||
if( (ok = (ttl == expected)) )
|
||||
printf(" [OK] No missing pops.\n");
|
||||
else printf(" [Failure] Missing pops!\n");
|
||||
}
|
||||
printf("Collector processed %d / %d (attempted: %d) pushed values\n", c.load(std::memory_order_relaxed), WORKERS, n.load(std::memory_order_relaxed));
|
||||
bool stopped = stop_workers->stop_requested();
|
||||
|
||||
if(!ok && !stopped)
|
||||
throw std::runtime_error("Invalid number of workers to valid collector `.pop()` calls (non-cancelled case.)");
|
||||
if(stopped)
|
||||
printf("The workers were cancelled by SIGINT, discrepency expected.\n");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
int main() {
|
||||
// Test sync-queue `fx::Queue<T>`
|
||||
fx::test::_so_qrtx<20>();
|
||||
}
|
||||
#endif // #if 1 // FEATURE-FX-TEST
|
Loading…
Reference in new issue