diff --git a/include/deal.II/base/initialization_check.h b/include/deal.II/base/initialization_check.h new file mode 100644 index 000000000000..93ec4f33b6b1 --- /dev/null +++ b/include/deal.II/base/initialization_check.h @@ -0,0 +1,211 @@ +// --------------------------------------------------------------------- +// +// Copyright (C) 2003 - 2023 by the deal.II authors +// +// This file is part of the deal.II library. +// +// The deal.II library is free software; you can use it, redistribute +// it, and/or modify it under the terms of the GNU Lesser General +// Public License as published by the Free Software Foundation; either +// version 2.1 of the License, or (at your option) any later version. +// The full text of the license can be found in the file LICENSE.md at +// the top level directory of deal.II. +// +// --------------------------------------------------------------------- + +#ifndef dealii_initialization_check_h +#define dealii_initialization_check_h + + +#include + +#include + +DEAL_II_NAMESPACE_OPEN + +/** + * @addtogroup threads + * @{ + */ + +namespace Threads +{ + /** + * This class is an optimized, thin wrapper around the + * std::call_once + * mechanism adding copy and move semantics to the std::once_flag + * mimicking the copy and move semantics of the fundamental bool type. + * + * The InitializationCheck helper ensures that lazy, on-demand + * initialization of some expensive data structure happens (a) in a + * thread-safe manner, and that (b) subsequent checks in hot paths are + * cheap. Intended usage: + * ``` + * class Consumer { + * public: + * void do_something() { + * initialization_check.check_and_initialize([&](){ + * // initialize payload; + * }); + * + * // do something with payload. + * } + * private: + * // payload + * InitializationCheck initialization_check; + * }; + * ``` + * Using InitializationCheck instead of std::call_once has a number of + * advantages: + * - The class implements proper copy and move semantics modeling a + * boolean (in contrast to std::once_flag, which is neither copyable + * nor movable). This is particularly desirable for our typical use + * case in deal.II where we want to initialize some payload exactly + * once (and simply copy or move it along with the container). + * - The "passive" call, i.e., when the object is already initialized is + * much cheaper than a call to std::call_once, or thread + * synchronization with a std::shared_mutex (because the + * check_and_initialize function is inlined and consists of a mere + * check of a boolean and conditional jump). + */ + class InitializationCheck + { + public: + /** + * Default Constructor. + */ + InitializationCheck() + : is_initialized(false) + {} + + + /** + * Copy constructor. + * + * We adopt the copy semantics of a plain boolean and simply copy the + * state. + */ + InitializationCheck(const InitializationCheck &other) + : is_initialized(other.is_initialized) + { + // + // The only legal operation for a std::once_flag is to default + // construct it which sets its internal state to "function not + // called". This is OK as we record the status whether we have + // already called the initialization function in the is_initialized + // boolean. + // + } + + + /** + * Move constructor. + * + * We adopt the copy semantics of a plain boolean and simply copy the + * state. + */ + InitializationCheck(InitializationCheck &&other) noexcept + : is_initialized(other.is_initialized) + { + // + // The only legal operation for a std::once_flag is to default + // construct it which sets its internal state to "function not + // called". This is OK as we record the status whether we have + // already called the initialization function in the is_initialized + // boolean. + // + } + + + /** + * Copy assignment. + * + * We adopt the copy semantics of a plain boolean and simply copy the + * state. + */ + InitializationCheck & + operator=(const InitializationCheck &other) + { + is_initialized = other.is_initialized; + + // + // Copy assignment might reset an initialized state with "not + // initialized". Thus, we have to reset the std::once_flag to + // "function not called". + // + flag.~once_flag(); + new (&flag) std::once_flag; + + return *this; + } + + + /** + * Move assignment. + * + * We adopt the copy semantics of a plain boolean and simply copy the + * state. + */ + InitializationCheck & + operator=(InitializationCheck &&other) noexcept + { + is_initialized = other.is_initialized; + + // + // Copy assignment might reset an initialized state with "not + // initialized". Thus, we have to reset the std::once_flag to + // "function not called". + // + flag.~once_flag(); + new (&flag) std::once_flag; + + return *this; + } + + + /** + * This function behaves similar to std::call_once: It checks whether + * it has been called before and if not ensures that the @p payload + * function is executed exactly once in a thread safe manner. + */ + template + DEAL_II_ALWAYS_INLINE inline void + check_and_initialize(const Payload &payload) + { + if (is_initialized == false) +#ifdef DEAL_II_HAVE_CXX20 + [[unlikely]] +#endif + { + std::call_once(flag, [&]() { + payload(); + is_initialized = true; + + // The standard guarantees a memory fence by ensuring a "total + // order" of passive and active calls to std::call_once. + }); + } + } + + + private: + /** + * A boolean recording the fact whether the check_and_initialized() + * function has been called. + */ + bool is_initialized; + + /** + * A std::once_flag object used for thread synchronization during + * initialization. + */ + std::once_flag flag; + }; +} // namespace Threads + +/** + * @} + */ + +DEAL_II_NAMESPACE_CLOSE +#endif