]> https://gitweb.dealii.org/ - dealii.git/commitdiff
Add TaskResult::try_emplace_task().
authorWolfgang Bangerth <bangerth@colostate.edu>
Fri, 19 Jul 2024 23:37:19 +0000 (17:37 -0600)
committerWolfgang Bangerth <bangerth@colostate.edu>
Sat, 20 Jul 2024 03:01:56 +0000 (21:01 -0600)
include/deal.II/base/task_result.h

index 6ebdf0da1cc74bedce3da6bd173566adb665ad72..c231e22a15ab16035f213ad7896af0b1835219fd 100644 (file)
@@ -238,10 +238,72 @@ namespace Threads
      * result are not put into the current object, it still means that that task
      * is working on data that is changing as it is working, with obviously
      * unpredictable results.)
+     *
+     * @note As mentioned above, it is a considered a bug to assign a task to
+     *   a TaskResult object that already has a task running. That means that
+     *   you will get in trouble if multiple threads (or multiple tasks running
+     *   concurrently) call this operator at the same time: One of these
+     *   threads will set a task, and the other threads will try the same but
+     *   because the background task is likely still running will encounter
+     *   an error. As a consequence, you cannot easily use this operator
+     *   from multiple threads. Use try_emplace_task() in that case.
      */
     void
     operator=(const Task<T> &t);
 
+    /**
+     * This function is similar to `operator=()` in that it associates a
+     * task with the current object if one has not been associated so far,
+     * but does not do so if a task is already assigned. For this to work,
+     * the object provided as argument to this function must be a "callable"
+     * (i.e., a object `creator` that can be called on a separate task via
+     * its `operator()`), rather than a Task object itself.
+     *
+     * As a consequence, code such as the following will work:
+     * @code
+     *   class LazyInt
+     *   {
+     *     public:
+     *       LazyInt () {} // no task assigned to task_result
+     *
+     *       int get () {
+     *         task_result.try_emplace_task( []() { return 42; } );
+     *         return task_result.value();
+     *       }
+     *
+     *     private:
+     *       TaskResult<int> task_result;
+     *   }
+     * @endcode
+     * In this context, the `LazyInt::get()` function is thread-safe, i.e., it
+     * can be called more than once from multiple threads. One of these threads
+     * -- namely, the first one to get into `try_emplace_task()` -- will create
+     * a task that calls the lambda function that returns `42` whereas all of
+     * the others will simply proceed to the call to `value()` that waits for
+     * the task to finish.
+     *
+     * On the other hand, implementing the `get()` function as
+     * @code
+     *       int get () {
+     *         task_result = Threads::new_task( []() { return 42; } );
+     *         return task_result.value();
+     *       }
+     * @endcode
+     * would have led to the errors mentioned above because `operator=` called
+     * from separate threads would have emplaced a task while another task
+     * is (possibly) still running.
+     *
+     * @note There is nothing that prevents you from concurrently calling
+     *   this function with *different* callables as arguments, i.e., with
+     *   functions that create non-identical objects. That is obviously
+     *   not the intent here since you can't control which callable will
+     *   eventually be turned into a task.
+     */
+    template <typename Callable>
+    void
+    try_emplace_task(const Callable &creator) const
+      DEAL_II_CXX20_REQUIRES((std::is_invocable_r_v<T, Callable>));
+
     /**
      * Reset the current object to a state as if it had been
      * default-constructed. For the same reasons as outlined
@@ -357,6 +419,35 @@ namespace Threads
   }
 
 
+  template <typename T>
+  template <typename Callable>
+  void
+  TaskResult<T>::try_emplace_task(const Callable &creator) const
+    DEAL_II_CXX20_REQUIRES((std::is_invocable_r_v<T, Callable>))
+  {
+    // If the result is already available, simply return.
+    if (result_is_available)
+      return;
+
+    // If the result was not available above, we need to go under a lock
+    // to check that perhaps it has appeared in the meantime. We again use
+    // the double-checking pattern:
+    {
+      std::lock_guard<std::mutex> lock(mutex);
+      if (result_is_available)
+        return;
+      else
+        // If there is no result, but there is a task, some other thread has
+        // emplaced it in the meantime and we can simply return
+        if (task.has_value())
+          return;
+        else
+          // If there is no task object, emplace one:
+          task = Threads::new_task(creator);
+    }
+  }
+
+
 
   template <typename T>
   inline void
@@ -401,11 +492,12 @@ namespace Threads
     // If we have waited before, then return immediately:
     if (result_is_available)
       return;
-    else // If we have not waited, wait now. We need to use the double-checking
-         // pattern to ensure that if two threads get to this place at the same
-         // time, one returns right away while the other does the work. Note
-         // that this happens under the lock, so only one thread gets to be in
-         // this code block at the same time:
+    else
+      // If we have not waited, wait now. We need to use the double-checking
+      // pattern to ensure that if two threads get to this place at the same
+      // time, one returns right away while the other does the work. Note
+      // that this happens under the lock, so only one thread gets to be in
+      // this code block at the same time:
       {
         std::lock_guard<std::mutex> lock(mutex);
         if (result_is_available)

In the beginning the Universe was created. This has made a lot of people very angry and has been widely regarded as a bad move.

Douglas Adams


Typeset in Trocchi and Trocchi Bold Sans Serif.