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