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