Skip to content

Commit

Permalink
base: add an Initializer class
Browse files Browse the repository at this point in the history
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
tamiko committed Oct 24, 2023
1 parent 9050964 commit 3c4f1cd
Showing 1 changed file with 211 additions and 0 deletions.
211 changes: 211 additions & 0 deletions include/deal.II/base/initialization_check.h
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

0 comments on commit 3c4f1cd

Please sign in to comment.