deal.II version GIT relicensing-6834-g5b78e6bcdf 2026-10-01 11:20:01+00:00
\(\newcommand{\dealvcentcolon}{\mathrel{\mathop{:}}}\) \(\newcommand{\dealcoloneq}{\dealvcentcolon\mathrel{\mkern-1.2mu}=}\) \(\newcommand{\jump}[1]{\left[\!\left[ #1 \right]\!\right]}\) \(\newcommand{\average}[1]{\left\{\!\left\{ #1 \right\}\!\right\}}\)
Loading...
Searching...
No Matches
lazy.h
Go to the documentation of this file.
1// -----------------------------------------------------------------------------
2//
3// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception OR LGPL-2.1-or-later
4// Copyright (C) 2023 - 2024 by the deal.II authors
5//
6// This file is part of the deal.II library.
7//
8// Detailed license information governing the source code and contributions
9// can be found in LICENSE.md and CONTRIBUTING.md at the top level directory.
10//
11// -----------------------------------------------------------------------------
12
13#ifndef dealii_lazy_h
14#define dealii_lazy_h
15
16
17#include <deal.II/base/config.h>
18
21#include <deal.II/base/mutex.h>
23
24#include <atomic>
25#include <mutex>
26#include <optional>
27#include <type_traits>
28
29
31
129template <typename T>
130DEAL_II_CXX20_REQUIRES((std::is_move_constructible_v<T> &&
131 std::is_move_assignable_v<T>))
132class Lazy
133{
134public:
139
140
146 Lazy(const Lazy &other);
147
148
155 Lazy(Lazy &&other) noexcept;
156
157
165 Lazy &
166 operator=(const Lazy &other);
167
168
177 Lazy &
178 operator=(Lazy &&other) noexcept;
179
180
184 void
185 reset() noexcept;
186
187
206 template <typename Callable>
207 void
208 ensure_initialized(const Callable &creator) const
209 DEAL_II_CXX20_REQUIRES((std::is_invocable_r_v<T, Callable>));
210
211
216 bool
217 has_value() const;
218
219
226 const T &
227 value() const;
228
229
247 template <typename Callable>
248 const T &
249 value_or_initialize(const Callable &creator) const
250 DEAL_II_CXX20_REQUIRES((std::is_invocable_r_v<T, Callable>));
251
255 std::size_t
256 memory_consumption() const;
257
258private:
262 mutable Threads::TaskResult<T> task_result;
263
264
269 mutable std::atomic<bool> object_is_initialized;
270};
271
277// ------------------------------- inline functions --------------------------
278
279#ifndef DOXYGEN
280
281template <typename T>
282DEAL_II_CXX20_REQUIRES((std::is_move_constructible_v<T> &&
283 std::is_move_assignable_v<T>))
284// NOLINTNEXTLINE(modernize-use-equals-default)
285inline Lazy<T>::Lazy()
286 : object_is_initialized(false)
287{}
288
289
290
291template <typename T>
292DEAL_II_CXX20_REQUIRES((std::is_move_constructible_v<T> &&
293 std::is_move_assignable_v<T>))
294inline Lazy<T>::Lazy(const Lazy &other)
295{
296 // If the other object has a value stored, then get it and set our
297 // own object to a copy of it:
298 if (other.has_value())
299 {
300 object_is_initialized.store(true);
301 task_result.emplace_object(other.value());
302 }
303 else
304 object_is_initialized.store(false);
305}
306
307
308
309template <typename T>
310DEAL_II_CXX20_REQUIRES((std::is_move_constructible_v<T> &&
311 std::is_move_assignable_v<T>))
312inline Lazy<T>::Lazy(Lazy &&other) noexcept
313 : task_result(std::move(other.task_result))
314{
315 object_is_initialized.store(other.object_is_initialized.load());
316
317 // Mark the other object as uninitialized.
318 other.object_is_initialized.store(false);
319}
320
321
322
323template <typename T>
324DEAL_II_CXX20_REQUIRES((std::is_move_constructible_v<T> &&
325 std::is_move_assignable_v<T>))
326inline Lazy<T> &Lazy<T>::operator=(const Lazy &other)
327{
328 // If the other object has a value stored, then get it and set our
329 // own object to a copy of it:
330 if (other.has_value())
331 {
332 object_is_initialized.store(true);
333 task_result.emplace_object(other.value());
334 }
335 else
336 {
337 object_is_initialized.store(false);
338 task_result.clear();
339 }
340
341 return *this;
342}
343
344
345
346template <typename T>
347DEAL_II_CXX20_REQUIRES((std::is_move_constructible_v<T> &&
348 std::is_move_assignable_v<T>))
349inline Lazy<T> &Lazy<T>::operator=(Lazy &&other) noexcept
350{
351 task_result = std::move(other.task_result);
352 object_is_initialized.store(other.object_is_initialized.load());
353
354 // Mark the other object as uninitialized.
355 other.object_is_initialized.store(false);
356
357 return *this;
358}
359
360
361
362template <typename T>
363DEAL_II_CXX20_REQUIRES((std::is_move_constructible_v<T> &&
364 std::is_move_assignable_v<T>))
365inline void Lazy<T>::reset() noexcept
366{
367 object_is_initialized.store(false);
368 task_result.clear();
369}
370
371
372template <typename T>
373DEAL_II_CXX20_REQUIRES((std::is_move_constructible_v<T> &&
374 std::is_move_assignable_v<T>))
375template <typename Callable>
377 void Lazy<T>::ensure_initialized(const Callable &creator) const
378 DEAL_II_CXX20_REQUIRES((std::is_invocable_r_v<T, Callable>))
379{
380 //
381 // Use Schmidt's double checking [1] for checking and initializing the
382 // object.
383 //
384 // [1] https://en.wikipedia.org/wiki/Double-checked_locking
385 //
386
387 //
388 // Check the object_is_initialized atomic with "acquire" semantics [1].
389 //
390 // This ensures that (a) all subsequent reads (of the object) are
391 // ordered after this check, and that (b) all writes to the object
392 // before the atomic bool was set to true with "release" semantics are
393 // visible on this thread.
394 //
395 // [1]
396 // https://en.cppreference.com/w/cpp/atomic/memory_order#Release-Acquire_ordering
397 //
398 if (!object_is_initialized.load(std::memory_order_acquire))
399# ifdef DEAL_II_HAVE_CXX20
400 [[unlikely]]
401# endif
402 {
403 // Check again. If this thread won the race to the lock then we
404 // would like to initialize the object. Otherwise another thread has
405 // already initialized the object and flipped the object_is_initialized
406 // bit. (Here, the initialization_mutex ensures consistent ordering
407 // with a memory fence, so we will observe the updated bool without
408 // acquire semantics.)
409 //
410 // Naively, we should think that we can check again for
411 // object_is_initialized to be true, and if it is false just execute
412 // the 'creator' function object. The problem with this approach is that
413 // if we have N worker threads and spawn N+1 tasks that all want to
414 // end querying the Lazy object, then N will be run right away of which
415 // N-1 will block. The one remaining task will have gotten into the
416 // locked section and run the 'creator' object. But if the 'creator'
417 // itself spawns tasks, the scheduler may decide to first execute
418 // the (N+1)st task from above which will then promptly get stuck
419 // here as well -- and we're in a deadlock situation.
420 //
421 // The solution to the problem is to use a scheme whereby the
422 // work we do in setting up 'creator' cannot block, and where we block
423 // below is in a context where it's the *scheduler* that blocks to ensure
424 // that it can continue to schedule tasks.
425 //
426 // This is what TaskResult::try_emplace_task() does:
427 if (!object_is_initialized.load(std::memory_order_relaxed))
428 task_result.try_emplace_task(creator);
429
430 // At this point, either this or another thread have emplaced a
431 // task and we wait for it to complete. If we do need to wait, then
432 // the waiting happens in the task scheduler, which can run other
433 // tasks in the meantime, ensuring progress:
434 task_result.join();
435
436 // At this point, we know that the task has completed (whether set by
437 // this or another thread), and we can flip the object_is_initialized
438 // boolean with "release" semantics [1].
439 //
440 // This ensures that the above move is visible on all threads
441 // before checking the atomic bool with acquire semantics.
442 // If another thread that had also been waiting in the join() call
443 // above has gotten here first and set the flag to 'true', that
444 // ok.
445 object_is_initialized.store(true, std::memory_order_release);
446 }
447
448 Assert(has_value(),
449 ExcMessage("The current object does not contain a valid object "
450 "even though we have just initialized it."));
451}
452
453
454template <typename T>
455DEAL_II_CXX20_REQUIRES((std::is_move_constructible_v<T> &&
456 std::is_move_assignable_v<T>))
458{
459 //
460 // In principle it would be sufficient to solely check the atomic<bool>
461 // object_is_initialized because the load() is performed with "acquire"
462 // semantics. But just in case let's check the object.has_value() boolean
463 // as well:
464 //
465 return (object_is_initialized && (task_result.empty() == false));
466}
467
468
469template <typename T>
470DEAL_II_CXX20_REQUIRES((std::is_move_constructible_v<T> &&
471 std::is_move_assignable_v<T>))
472inline DEAL_II_ALWAYS_INLINE const T &Lazy<T>::value() const
473{
474 Assert(
475 has_value(),
477 "value() has been called but the contained object has not been "
478 "initialized. Did you forget to call 'ensure_initialized()' first?"));
479
480 return task_result.value();
481}
482
483
484template <typename T>
485DEAL_II_CXX20_REQUIRES((std::is_move_constructible_v<T> &&
486 std::is_move_assignable_v<T>))
487template <typename Callable>
489 const Callable &creator) const
490 DEAL_II_CXX20_REQUIRES((std::is_invocable_r_v<T, Callable>))
491{
492 ensure_initialized(creator);
493 return task_result.value();
494}
495
496
497template <typename T>
498DEAL_II_CXX20_REQUIRES((std::is_move_constructible_v<T> &&
499 std::is_move_assignable_v<T>))
500std::size_t Lazy<T>::memory_consumption() const
501{
502 return MemoryConsumption::memory_consumption(task_result) + //
503 sizeof(*this) - sizeof(task_result);
504}
505
506#endif
507
509#endif
Definition lazy.h:133
const T & value_or_initialize(const Callable &creator) const
void ensure_initialized(const Callable &creator) const
void reset() noexcept
bool has_value() const
std::atomic< bool > object_is_initialized
Definition lazy.h:269
std::size_t memory_consumption() const
const T & value() const
Lazy(const Lazy &other)
Lazy & operator=(Lazy &&other) noexcept
Threads::TaskResult< T > task_result
Definition lazy.h:262
Lazy(Lazy &&other) noexcept
Lazy & operator=(const Lazy &other)
#define DEAL_II_ALWAYS_INLINE
Definition config.h:166
#define DEAL_II_NAMESPACE_OPEN
Definition config.h:38
#define DEAL_II_CXX20_REQUIRES(condition)
Definition config.h:249
#define DEAL_II_NAMESPACE_CLOSE
Definition config.h:39
#define Assert(cond, exc)
static ::ExceptionBase & ExcMessage(std::string arg1)
constexpr char T
std::enable_if_t< std::is_fundamental_v< T >, std::size_t > memory_consumption(const T &t)
STL namespace.