deal.II version GIT relicensing-2287-g6548a49e0a 2024-12-20 18:30:00+00:00
|
#include <deal.II/base/task_result.h>
Public Member Functions | |
TaskResult () | |
TaskResult (const Task< T > &task) | |
TaskResult (const TaskResult< T > &)=delete | |
TaskResult (TaskResult< T > &&other) noexcept | |
~TaskResult () | |
TaskResult & | operator= (const TaskResult &)=delete |
TaskResult & | operator= (TaskResult &&other) noexcept |
void | operator= (const Task< T > &t) |
template<typename Callable > | |
void | try_emplace_task (const Callable &creator) const |
void | emplace_object (const T &t) |
void | emplace_object (T &&t) |
void | clear () |
void | join () const |
const T & | value () const |
bool | empty () const |
Private Attributes | |
std::atomic< bool > | result_is_available |
std::optional< Task< T > > | task |
std::optional< T > | task_result |
std::mutex | mutex |
A class that represents the outcome of a Threads::Task. The class is used as a member variable or local variable when something is computed on a separate task in the background. For example, a class's constructor may want to set up some expensive data structures that are not typically used right away in the next line of the function that created the object, but will be useful later; in such a case, it may defer computation of that data to a separate task that will be scheduled whenever a CPU core is available, and the result of that deferred computation will become available through a variable of the current type.
In some regard, class TaskResult is similar to class Lazy in that it defers construction of an object to a later point while already giving it a home. The difference is simply that TaskResult has the creation job already scheduled whereas Lazy defers creation to the first use. Lazy is therefore a better choice whenever the object referenced may or may not ever be used, say for some obscure functionality of a class that is rarely required. On the other hand, TaskResult is used for member variables that are definitely needed, but perhaps not right away.
This class has an interface that is quite similar to the Threads::Task class itself, and object of which is provided to the constructor. The key difference is that the Task class describes the task that computes a result, whereas the current TaskResult class describes the result that is being computed by the task. The main practical differences between these perspectives are what happens when you want to copy an object: Copying a Task object results in two objects that are both referencing the same task, with the same returned object when the task has finished. On the other hand, copying a TaskResult object after the computing task has finished results in two copies of the returned object.
This class can also be compared with std::future
. That class also represents the result of a pending operation, but it lacks the specifics of what kind of operation that is (in particular, it does not know whether a task, a thread, or a std::packaged_task
will eventually produce the value). As a consequence, it can wait for the result to become available, but it lacks the knowledge to detect certain common programming mistakes such as those described in the documentation of the destructor of this class, or of this class's operator=()
. Another significant difference is that one can only call std::future::get()
once, whereas one can call Threads::Task::return_value() as many times as desired. It is, thus, comparable to the std::shared_future
class. However, std::shared_future
can not be used for types that can not be copied – a particular restriction for std::unique_ptr
, for example.
Definition at line 86 of file task_result.h.
|
inline |
Default constructor. An object of this kind is not (presently) associated with a task, and so cannot be asked for that task's result.
Definition at line 94 of file task_result.h.
|
inline |
A constructor that takes a Task object and initializes the current object to be the result of that task.
Definition at line 102 of file task_result.h.
|
delete |
Copy constructor. Because the result of a currently still running task is a unique object, TaskResult objects cannot be copied.
|
noexcept |
Move constructor. Following this call, the newly created object represents the task's result, whereas the old object no longer represents anything and is left as if default-constructed.
Threads::TaskResult< T >::~TaskResult | ( | ) |
Destructor. If the current object was associated with a task, then the destructor will throw an error if that task is still running. This is because typically, the task will still be working on data that is already gone away, or will shortly go away, and that means that that task will likely encounter unpredictable outcomes. As an example, consider the following code:
Here, the constructor ~Complicated()
destroys the object's member variables, and then calls the destructor of hash_value
. If at the time we get to the latter destructor the background task to compute a hash value is still running, then this is strictly speaking not a problem for the hash_value
variable (we could simply abandon the background task and its result), but it is indicative of a programming bug where that background task is now working on invalid memory. To catch these things, the destructor of this class will throw an error if it encounters a situation where the task it is associated with is still running.
The correct solution is to wait in the surrounding class's destructor for the background task to finish before tearing down data structures:
(One could have designed this class in such a way that the destructor waits for the task to complete. But this is error prone, because C++ prescribes that destructors of member variables are called in reverse order in which they are declared, and so if this destructor is called by the compiler as part of a surrounding class's destructor, the destructors of all member variables have already run by the time we get to wait for the background task – but the background task is likely still working on these data members, and that presents a probable bug. Instead, we force the destructor of the surrounding class to explicitly wait for the task to complete before reaching the end of the destructor at which the compiler inserts the calls to the destructors of the class's member variables.)
|
delete |
Copy operator. Like the copy constructor, this function is deleted because TaskResult corresponds to the output of a specific task, not a copy of that tasks's outcome.
|
noexcept |
Move assignment operator. Following this call, the newly created object represents the task's result, whereas the old object no longer represents anything and is left as if default-constructed.
void Threads::TaskResult< T >::operator= | ( | const Task< T > & | t | ) |
Copy assignment operator from a Task object. By assigning the Task object to the current object, the current object is set to represent the result of that task.
When calling this operator, the current object will no longer represent the result of a previous Task. If the previously associated task is still running, this function throws an exception. This is because assigning a new task to this object when the previous task is still running is likely a bug: In most cases, the new and old tasks are both operating on the state of another object; the fact that the old task is still running often indicates that it is working on data that has been changed underneath. To illustrate this, case, consider the following code snippet:
Here, in frobnicate()
, member variables are updated according to what the frobnicate()
operation represents, and then the hash_value
variable is updated as well, but in the background. The issue is that if the background task started in the constructor is still running, then that background task is working on data that is being changed beneath it, likely resulting in unpredictable outcomes. The correct approach would therefore be to write frobnicate()
as follows:
The call to join() ensures that the program waits for the previous task to finish before starting to modify the underlying data. (Calling clear()
might seem like a useful alternative in that it simply abandons the first background task, but while that ensures that the first task's 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.)
void Threads::TaskResult< T >::try_emplace_task | ( | const Callable & | creator | ) | const |
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:
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
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.
void Threads::TaskResult< T >::emplace_object | ( | const T & | t | ) |
Instead of letting a task compute the object stored by this instance of Lazy, just copy the object passed as an argument and use it instead.
As for several other member functions, this function can only be called if there is not currently a running task whose result is supposed to be used.
void Threads::TaskResult< T >::emplace_object | ( | T && | t | ) |
Instead of letting a task compute the object stored by this instance of Lazy, just move the object passed as an argument and use it instead.
As for several other member functions, this function can only be called if there is not currently a running task whose result is supposed to be used.
void Threads::TaskResult< T >::clear | ( | ) |
Reset the current object to a state as if it had been default-constructed. For the same reasons as outlined in the documentation of the destructor and of the assignment operator, this function considers it an error (and throws an exception) if there is still a currently running task associated with the object. Because you cannot know when a task associated with an object finishes, the practical realization of there not being a currently still running task is that you can only call this function on an object that has an associated task after you have either called join() or asked for the return value by having called value() (which internally calls join()).
void Threads::TaskResult< T >::join | ( | ) | const |
If this object is associated with a task, wait for it to finish. If it isn't associated with a task, just return.
const T & Threads::TaskResult< T >::value | ( | ) | const |
Return a reference to the object computed by the task. This is always a const
reference to reflect the semantics that this object represents what the Threads::Task computed (which is what it is, and cannot be changed later on).
bool Threads::TaskResult< T >::empty | ( | ) | const |
Return whether the object has an associated task result object or not. Only objects that are default-initialized, or for which clear() has been called, or that have been moved from, will return true
in this function.
|
mutableprivate |
An atomic flag that allows us to test whether the task has finished and the result is available.
Definition at line 399 of file task_result.h.
|
mutableprivate |
An object that references the task that computes the result TaskResult stores. Once the task has finished, and the result has been moved out of the task
object and into task_result
, the task
object is reset – that is, we destroy all traces of references to the Thread::Task that computed the result.
Definition at line 408 of file task_result.h.
|
mutableprivate |
The object that results from running the background task.
Definition at line 413 of file task_result.h.
|
mutableprivate |
A lock object that guards access to all of the mutable
objects above.
Definition at line 418 of file task_result.h.