forked from dealii/dealii
-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Implement an optimized, thin wrapper around the <a href="https://en.cppreference.com/w/cpp/thread/call_once">std::call_once mechanism</a> 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: - 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).
- Loading branch information
Showing
1 changed file
with
211 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 <deal.II/base/config.h> | ||
|
||
#include <mutex> | ||
|
||
DEAL_II_NAMESPACE_OPEN | ||
|
||
/** | ||
* @addtogroup threads | ||
* @{ | ||
*/ | ||
|
||
namespace Threads | ||
{ | ||
/** | ||
* This class is an optimized, thin wrapper around the | ||
* <a href="https://en.cppreference.com/w/cpp/thread/call_once">std::call_once | ||
* mechanism</a> 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 <typename Payload> | ||
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 |