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
task_result.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_task_result_h
14#define dealii_task_result_h
15
16
17#include <deal.II/base/config.h>
18
21
22#include <atomic>
23#include <mutex>
24#include <optional>
25
27
33namespace Threads
34{
83 template <typename T>
85 {
86 public:
93 : result_is_available(false)
94 {}
95
101 : result_is_available(false)
102 , task(task)
103 {
104 // It is conceivable that the task has already finished if we
105 // come to this point, but that is not important to us here:
106 // we will simply find out once someone calls get()
107 }
108
114 TaskResult(const TaskResult<T> &) = delete;
115
122 std::is_move_constructible_v<T> &&std::is_move_assignable_v<T>);
123
182
188 TaskResult &
189 operator=(const TaskResult &) = delete;
190
196 TaskResult &
198 std::is_move_constructible_v<T> &&std::is_move_assignable_v<T>);
199
266 void
268
317 template <typename Callable>
318 void
319 try_emplace_task(const Callable &creator) const
320 DEAL_II_CXX20_REQUIRES((std::is_invocable_r_v<T, Callable>));
321
331 void
332 emplace_object(const T &t)
333 DEAL_II_CXX20_REQUIRES((std::is_copy_constructible_v<T> ||
334 std::is_copy_assignable_v<T>));
335
345 void
347 DEAL_II_CXX20_REQUIRES((std::is_copy_constructible_v<T> ||
348 std::is_copy_assignable_v<T>));
349
364 void
366
371 void
372 join() const;
373
380 const T &
381 value() const;
382
389 bool
390 empty() const;
391
392 private:
397 mutable std::atomic<bool> result_is_available;
398
406 mutable std::optional<Task<T>> task;
407
411 mutable std::optional<T> task_result;
412
416 mutable std::mutex mutex;
417 };
418
419
420 // ------------------------------- inline functions --------------------------
421
422#ifndef DOXYGEN
423
424 template <typename T>
425 inline TaskResult<T>::TaskResult(TaskResult<T> &&other) noexcept
427 std::is_move_constructible_v<T> &&std::is_move_assignable_v<T>)
428 {
429 // First lock the other object, then move the members of the other
430 // object and reset it. Note that we do not have to wait for
431 // the other object's task to finish (nor should we).
432 std::scoped_lock lock(other.mutex);
433
434 result_is_available = other.result_is_available.load();
435 other.result_is_available = false;
436
437 task = std::move(other.task);
438 other.task.reset();
439
440 task_result = std::move(other.task_result);
441 other.task_result.reset();
442 }
443
444
445
446 template <typename T>
448 {
449 // Ensure that there is no currently running task. As
450 // documented, we consider this an error. Since clear()
451 // also checks for this error, we can just defer to that function:
452 clear();
453 }
454
455
456
457 template <typename T>
458 inline void
459 TaskResult<T>::operator=(const Task<T> &t)
460 {
461 // First ensure that there is no currently running task. As
462 // documented, we consider this an error. Since clear()
463 // also checks for this error, we can just defer to that function:
464 clear();
465
466 // Having established that there is no previous task still running,
467 // set the current task as the one we're waiting for:
468 {
469 std::scoped_lock lock(mutex);
470 task = t;
471 }
472 }
473
474
475 template <typename T>
476 inline TaskResult<T> &
477 TaskResult<T>::operator=(TaskResult<T> &&other) noexcept
479 std::is_move_constructible_v<T> &&std::is_move_assignable_v<T>)
480 {
481 // First clear the current object before we put new content into it:
482 clear();
483
484 // Then lock the other object and move the members of the other
485 // object, and finally reset it. Note that we do not have to wait for
486 // the other object's task to finish (nor should we): We may simply
487 // inherit the other object's task.
488 std::scoped_lock lock(other.mutex);
489
490 result_is_available = other.result_is_available.load();
491 other.result_is_available = false;
492
493 task = std::move(other.task);
494 other.task.reset();
495
496 task_result = std::move(other.task_result);
497 other.task_result.reset();
498
499 return *this;
500 }
501
502
503
504 template <typename T>
505 template <typename Callable>
506 void
507 TaskResult<T>::try_emplace_task(const Callable &creator) const
508 DEAL_II_CXX20_REQUIRES((std::is_invocable_r_v<T, Callable>))
509 {
510 // If the result is already available, simply return.
511 if (result_is_available)
512 return;
513
514 // If the result was not available above, we need to go under a lock
515 // to check that perhaps it has appeared in the meantime. We again use
516 // the double-checking pattern:
517 {
518 std::scoped_lock lock(mutex);
519 if (result_is_available)
520 return;
521 else
522 // If there is no result, but there is a task, some other thread has
523 // emplaced it in the meantime and we can simply return
524 if (task.has_value())
525 return;
526 else
527 // If there is no task object, emplace one:
528 task = Threads::new_task(creator);
529 }
530 }
531
532
533
534 template <typename T>
535 inline void
537 DEAL_II_CXX20_REQUIRES((std::is_copy_constructible_v<T> ||
538 std::is_copy_assignable_v<T>))
539 {
540 clear();
541 task_result = t;
542 result_is_available = true;
543 }
544
545
546 template <typename T>
547 inline void
549 DEAL_II_CXX20_REQUIRES((std::is_copy_constructible_v<T> ||
550 std::is_copy_assignable_v<T>))
551 {
552 clear();
553 task_result = std::move(t);
554 result_is_available = true;
555 }
556
557
558 template <typename T>
559 inline void
561 {
562 std::scoped_lock lock(mutex);
563
564 if (result_is_available)
565 {
566 // First make clear that the result is no longer available, then
567 // reset the object:
568 result_is_available = false;
569 task_result.reset();
570 }
571 else
572 Assert(task.has_value() == false,
573 ExcMessage("You cannot destroy a TaskResult object "
574 "while it is still waiting for its associated task "
575 "to finish. See the documentation of this class' "
576 "destructor for more information."));
577 }
578
579
580
581 template <typename T>
582 inline void
583 TaskResult<T>::join() const
584 {
585 Assert(empty() == false,
586 ExcMessage("You can't join a TaskResult object that has not "
587 "been associated with a task."));
588
589 // If we have waited before, then return immediately:
590 if (result_is_available)
591 return;
592 else
593 // If we have not waited, wait now. We need to use the double-checking
594 // pattern to ensure that if two threads get to this place at the same
595 // time, one returns right away while the other does the work. Note
596 // that this happens under the lock, so only one thread gets to be in
597 // this code block at the same time:
598 {
599 std::scoped_lock lock(mutex);
600 if (result_is_available)
601 return;
602 else
603 {
604 // The object is not empty and it has not received its result yet.
605 // So it must have a task object:
606 Assert(task.has_value(), ExcInternalError());
607
608 task.value().join();
609 task_result = std::move(task.value().return_value());
610 task.reset();
611
612 result_is_available = true;
613 }
614 }
615 }
616
617
618
619 template <typename T>
620 inline bool
622 {
623 // If we have waited for a task to complete, then the object is not empty:
624 if (result_is_available)
625 return false;
626 // Otherwise, if result_is_available has not been set, but we have a task
627 // associated (i.e., the task is still running, or at least we haven't
628 // waited for it to complete), then the object is also not empty:
629 else if (task.has_value())
630 return false;
631 else
632 // If when we asked above we had not joined a task, and if there was
633 // no task currently associated with the object, then one of two cases
634 // could have happened: either, there never was a task, and the object
635 // is consequently empty. Or there was a task and somewhere between the
636 // checks above and now, join() has flipped the state to
637 // result_is_available==true and task.has_value()==false. We can
638 // check that, but only under a lock.
639 {
640 std::scoped_lock lock(mutex);
641 if (result_is_available)
642 return false;
643 else
644 // We know from getting into the above 'else that no task was
645 // associated with this object at the time. This cannot have
646 // changed since then in a way that is thread-safe (i.e., by
647 // way of other 'const' functions), so if the result is still
648 // not available, then the object must necessarily be empty:
649 return true;
650 }
651 }
652
653
654
655 template <typename T>
656 inline const T &
658 {
659 Assert(empty() == false,
661 "You can't ask for the result of a TaskResult object that "
662 "has not been associated with a task."));
663
664 if (!result_is_available)
665 join();
666 return task_result.value();
667 }
668
669#endif
670
671} // namespace Threads
672
673
681#endif
TaskResult & operator=(TaskResult &&other) noexcept
void emplace_object(const T &t)
void operator=(const Task< T > &t)
TaskResult(const TaskResult< T > &)=delete
TaskResult(TaskResult< T > &&other) noexcept
TaskResult & operator=(const TaskResult &)=delete
const T & value() const
std::optional< Task< T > > task
std::optional< T > task_result
void emplace_object(T &&t)
bool empty() const
TaskResult(const Task< T > &task)
void try_emplace_task(const Callable &creator) const
void join() const
std::atomic< bool > result_is_available
#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 & ExcInternalError()
static ::ExceptionBase & ExcMessage(std::string arg1)
Task< RT > new_task(const std::function< RT()> &function)
constexpr char T