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