Reference documentation for deal.II version 9.6.0
\(\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
124 TaskResult(TaskResult<T> &&other) noexcept;
125
183 ~TaskResult();
184
190 TaskResult &
191 operator=(const TaskResult &) = delete;
192
198 TaskResult &
199 operator=(TaskResult &&) noexcept;
200
267 void
268 operator=(const Task<T> &t);
269
318 template <typename Callable>
319 void
320 try_emplace_task(const Callable &creator) const
321 DEAL_II_CXX20_REQUIRES((std::is_invocable_r_v<T, Callable>));
322
337 void
338 clear();
339
344 void
345 join() const;
346
353 const T &
354 value() const;
355
362 bool
363 empty() const;
364
365 private:
370 mutable std::atomic<bool> result_is_available;
371
379 mutable std::optional<Task<T>> task;
380
384 mutable std::optional<T> task_result;
385
389 mutable std::mutex mutex;
390 };
391
392
393 // ------------------------------- inline functions --------------------------
394
395
396 template <typename T>
397 inline TaskResult<T>::TaskResult(TaskResult<T> &&other) noexcept
398 {
399 // First lock the other object, then move the members of the other
400 // object and reset it. Note that we do not have to wait for
401 // the other object's task to finish (nor should we).
402 std::lock_guard<std::mutex> lock(other.mutex);
403
404 result_is_available = other.result_is_available.load();
405 other.result_is_available = false;
406
407 task = std::move(other.task);
408 other.task.reset();
409
410 task_result = std::move(other.task_result);
411 other.task_result.reset();
412 }
413
414
415
416 template <typename T>
418 {
419 // Ensure that there is no currently running task. As
420 // documented, we consider this an error. Since clear()
421 // also checks for this error, we can just defer to that function:
422 clear();
423 }
424
425
426
427 template <typename T>
428 inline void
430 {
431 // First ensure that there is no currently running task. As
432 // documented, we consider this an error. Since clear()
433 // also checks for this error, we can just defer to that function:
434 clear();
435
436 // Having established that there is no previous task still running,
437 // set the current task as the one we're waiting for:
438 {
439 std::lock_guard<std::mutex> lock(mutex);
440 task = t;
441 }
442 }
443
444
445 template <typename T>
446 inline TaskResult<T> &
448 {
449 // First clear the current object before we put new content into it:
450 clear();
451
452 // Then lock the other object and move the members of the other
453 // object, and finally reset it. Note that we do not have to wait for
454 // the other object's task to finish (nor should we): We may simply
455 // inherit the other object's task.
456 std::lock_guard<std::mutex> lock(other.mutex);
457
458 result_is_available = other.result_is_available.load();
459 other.result_is_available = false;
460
461 task = std::move(other.task);
462 other.task.reset();
463
464 task_result = std::move(other.task_result);
465 other.task_result.reset();
466
467 return *this;
468 }
469
470
471
472 template <typename T>
473 template <typename Callable>
474 void
475 TaskResult<T>::try_emplace_task(const Callable &creator) const
476 DEAL_II_CXX20_REQUIRES((std::is_invocable_r_v<T, Callable>))
477 {
478 // If the result is already available, simply return.
480 return;
481
482 // If the result was not available above, we need to go under a lock
483 // to check that perhaps it has appeared in the meantime. We again use
484 // the double-checking pattern:
485 {
486 std::lock_guard<std::mutex> lock(mutex);
488 return;
489 else
490 // If there is no result, but there is a task, some other thread has
491 // emplaced it in the meantime and we can simply return
492 if (task.has_value())
493 return;
494 else
495 // If there is no task object, emplace one:
496 task = Threads::new_task(creator);
497 }
498 }
499
500
501
502 template <typename T>
503 inline void
505 {
506 std::lock_guard<std::mutex> lock(mutex);
507
509 {
510 // First make clear that the result is no longer available, then
511 // reset the object:
512 result_is_available = false;
513 task_result.reset();
514 }
515 else
516 Assert(task.has_value() == false,
517 ExcMessage("You cannot destroy a TaskResult object "
518 "while it is still waiting for its associated task "
519 "to finish. See the documentation of this class' "
520 "destructor for more information."));
521 }
522
523
524
525 template <typename T>
526 inline void
528 {
529 Assert(empty() == false,
530 ExcMessage("You can't join a TaskResult object that has not "
531 "been associated with a task."));
532
533 // If we have waited before, then return immediately:
535 return;
536 else
537 // If we have not waited, wait now. We need to use the double-checking
538 // pattern to ensure that if two threads get to this place at the same
539 // time, one returns right away while the other does the work. Note
540 // that this happens under the lock, so only one thread gets to be in
541 // this code block at the same time:
542 {
543 std::lock_guard<std::mutex> lock(mutex);
545 return;
546 else
547 {
548 // The object is not empty and it has not received its result yet.
549 // So it must have a task object:
550 Assert(task.has_value(), ExcInternalError());
551
552 task.value().join();
553 task_result = std::move(task.value().return_value());
554 task.reset();
555
556 result_is_available = true;
557 }
558 }
559 }
560
561
562
563 template <typename T>
564 inline bool
566 {
567 // If we have waited for a task to complete, then the object is not empty:
569 return false;
570 // Otherwise, if result_is_available has not been set, but we have a task
571 // associated (i.e., the task is still running, or at least we haven't
572 // waited for it to complete), then the object is also not empty:
573 else if (task.has_value())
574 return false;
575 else
576 // If when we asked above we had not joined a task, and if there was
577 // no task currently associated with the object, then one of two cases
578 // could have happened: either, there never was a task, and the object
579 // is consequently empty. Or there was a task and somewhere between the
580 // checks above and now, join() has flipped the state to
581 // result_is_available==true and task.has_value()==false. We can
582 // check that, but only under a lock.
583 {
584 std::lock_guard<std::mutex> lock(mutex);
586 return false;
587 else
588 // We know from getting into the above 'else that no task was
589 // associated with this object at the time. This cannot have
590 // changed since then in a way that is thread-safe (i.e., by
591 // way of other 'const' functions), so if the result is still
592 // not available, then the object must necessarily be empty:
593 return true;
594 }
595 }
596
597
598
599 template <typename T>
600 inline const T &
602 {
603 Assert(empty() == false,
605 "You can't ask for the result of a TaskResult object that "
606 "has not been associated with a task."));
607
609 join();
610 return task_result.value();
611 }
612} // namespace Threads
613
621#endif
TaskResult(const TaskResult< T > &)=delete
TaskResult & operator=(const TaskResult &)=delete
const T & value() const
std::optional< Task< T > > task
std::optional< T > task_result
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:503
#define DEAL_II_CXX20_REQUIRES(condition)
Definition config.h:177
#define DEAL_II_NAMESPACE_CLOSE
Definition config.h:504
#define Assert(cond, exc)
static ::ExceptionBase & ExcInternalError()
static ::ExceptionBase & ExcMessage(std::string arg1)
Task< RT > new_task(const std::function< RT()> &function)
STL namespace.