Skip to content

Creation of conditional deferred work

David Hollman edited this page Nov 28, 2017 · 4 revisions

create_work_if

Introduction

DARMA allows to create deferred work that is executed only if certain conditions are met. Such construct is equivalent to if-then-else statements in a generic programming language. Conditional concurrent work is created by calling function create_work_if, which is defined in darma/interface/app/create_work_if.h. The signature of this function is:

template <typename Functor = meta::nonesuch, typename... Args>
auto create_work_if(Args&& ... args);

create_work_if can be invoked using either a functor or a lambda. When a functor is used, its type should be explicitly specified. The call to create_work_if creates and returns an object of type _create_work_if_helper. This is a templated structure that is specialized for functors and lambdas. The two specializations are defined in darma/impl/create_work/create_if_then_functor.h and darma/impl/create_work/create_if_then_lambda.h respectively. This object is created by simply forwarding args, the variadic pack of arguments used to invoke create_work_if. A detailed description of _create_work_if_helper is provided in the following sections. For the sake of simplicity, the subsequent discussion will focus on the functor version. Before continuing, it is worth noting that create_work_if is used to specify the conditions to check in deciding whether to perform deferred work or not. The deferred work to be executed if such conditions are satisfied should be specified by calling method then_ on the object returned by create_work_if. Calls to create_work_if should have the following appearance:

create_work_if<FunctorType_If>(args_if...).then_<FunctorType_Then>(args_then...);

Similarly to an if-then-else statement, an else condition can also be specified by invoking else_ on the object returned by the call to then_.

The _create_work_if_helper struct

_create_work_if_helper is a templated struct that is specialized for functors and lambdas. This struct has the following public members:

static constexpr auto is_lambda_callable;
std::tuple<Args&&...> args_fwd_tup;   

where is_lambda_callable is set to false (true) for the functor (lambda) case, and args_fwd_tup stores the arguments used to invoke create_work_if. The constructor of _create_work_if_helper, which is invoked by create_work_if, simply initializes args_fwd_tup using perfect forwarding. _create_work_if_helper also has a templated member function then_. The signature of this function is:

template<typename ThenFunctor=meta::nonesuch, typename... ThenArgs>
auto then_(ThenArgs&&... args) &&

As mentioned in the previous section, then_<FunctorType_Then> is used to specify the deferred work to be executed should the conditions specified in the if section of the create_work_if construct be satisfied. then_ takes as input a variadic pack of arguments, corresponding to the arguments passed to the functor associated with the then section of the construct. This function creates an object of type _create_work_then_helper by perfect forwarding its input arguments and returns the object so created. As it will become clearer in the following paragraphs, this object is responsible for triggering the capture mechanism , should a else_ clause not be provided, or to allow the specification of such clause.

The _create_work_then_helper struct

It was mentioned that method then_ of _create_work_if_helper is used to specify the deferred work to be conditionally executed should the if clause be satisfied. This method takes as inputs the arguments of the functor / lambda to be conditionally executed and use them to create and return an object of type _create_work_then_helper. This struct, which is defined in darma/impl/create_work/create_if_then_functor.h (darma/impl/create_work/create_if_then_lambda.h for lambda tasks), is templated on IfHelper, the _create_work_if_helper struct used to create it, Functor, the functor to be conditionally executed, and Args, the arguments' types of such functor. The member variables of this struct are:

  • IfHelper if_helper: stores the instance of _create_work_if_helper used to initialize _create_work_then_helper;
  • args_fwd_tuple_t args_fwd_tup: tuple of the arguments used to initialize _create_work_if_helper;
  • bool else_invoked: defaulted to false, it is set to true if an alternative deferred work is specified through an else clause. This can be done by invoking method else_ of _create_work_then_helper.

Other than its constructor and method else_, the definition of _create_work_then_helper also provides a non-trivial destructor. Such destructor is in charge of triggering the capture mechanism and registering the task associated with the if clause, should an else clause not be provided. If an else clause is provided, these responsibilities are assigned to the destructor of _create_work_else_helper, the type of the object returned by method else_. Assuming method else_ is not invoked, the destructor of _create_work_then_helper creates a shared pointer to an object of type IfThenElseCaptureManager and then call its method finish_construction_and_register_if_task. The IfThenElseCaptureManager class will be described in the following section. Before proceeding, it is worth summarizing the operations performed during create_work_if. A call to this method returns an object of type _create_work_if_helper, which is then used to create a second object of type _create_work_then_helper by calling its method then_. If desired, this second object can be used to create a third object, _create_work_else_helper, by calling its method else_. This object remains anonymous and is destroyed immediately after being created. Its destruction triggers the capture mechanism, whose implementation will be discussed next.

The IfThenElseCaptureManager struct

The IfThenElseCaptureManager struct is defined in darma/impl/create_work/create_if_then.h. Its specialization for functors is:

template <typename IfCallableT, typename... IfArgsT, bool IfIsLambda,
          typename ThenCallableT, typename... ThenArgsT, bool ThenIsLambda,
          typename ElseCallableT, typename... ElseArgsT, bool ElseIsLambda,
          bool ElseGiven
>
struct IfThenElseCaptureManager<IfCallableT, std::tuple<IfArgsT...>, IfIsLambda,
                                ThenCallableT, std::tuple<ThenArgsT...>, ThenIsLambda,
                                ElseCallableT, std::tuple<ElseArgsT...>, ElseIsLambda,
                                ElseGiven
> : public CaptureManager, public IfThenElseCaptureManagerSetupHelper

The template parameters of IfThenElseCaptureManager are:

  • IfCallableT: type of the callable (functor in this case) associated with the if section of the if-then-else construct;
  • std::tuple<IfArgsT...>: tuple of the arguments types taken by IfCallableT;
  • IfIsLambda: boolean indicating whether IfCallableT is a functor or a lambda;
  • ThenCallableT: type of the callable (functor in this case) associated with the then section of the if-then-else construct;
  • std::tuple<ThenArgsT...>: tuple of the arguments types taken by ThenCallableT;
  • ThenIsLambda: boolean indicating whether ThenCallableT is a functor or a lambda;
  • ElseCallableT: type of the callable (functor in this case) associated with the else section of the if-then-else construct;
  • std::tuple<ElseArgsT...>: tuple of the arguments types taken by ElseCallableT;
  • ElseIsLambda: boolean indicating whether ElseCallableT is a functor or a lambda;
  • ElseGiven: boolean indicating whether an else section has been given or not.

IfThenElseCaptureManager has the following private members:

  • std::unique_ptr<if_task_t> if_task_;
  • std::unique_ptr<then_task_t> then_task_;
  • std::unique_ptr<else_task_t> else_task_.

Here, if_task_t, then_task_t and else_task_t are private type aliases that correspond to either IfFunctorTask, ThenFunctorTask, ElseFunctorTask, or IfLambdaTask, ThenLambdaTask, ElseLambdaTask respectively, depending on the values of IfIsLambda, ThenIsLambda and ElseIsLambda. The members if_task_, then_task_ and else_task_ are initialized during the construction of IfThenElseCaptureManager, with else_task_ being set to nullptr if no else_ section was defined. The types aliased by if_task_t, then_task_t and else_task_t inherits from either FunctorTask or LambdaTask: their creation will trigger the dependency capture mechanism. Although this mechanism is similar to the one described for create_work, there is a fundamental difference that stem from the use of IfThenElseCaptureManager as the capture manager in create_work_if. Method do_capture now simply stores the captured and source and continuing access handles for the if, then_ and else_ sections in three distinct std::map objects, if_captures_, then_captures_ and else_captures_. The actual creation of the captured use holder object and its registration as a task dependency is done in a later stage: this is made necessary by the fact that the then_ and else_ sections of the if-then-else statement are not necessarily executed. For what concerns the if_ section, they are triggered by the call to method finish_construction_and_register_if_task of IfThenElseCaptureManager at the end of the destructor of the temporary object of type _create_work_then_helper (_create_work_else_helper if the else_ section was specified) created by create_work_if(...).then_(...). Regarding the then_ and else_ sections, these actions are triggered by calling methods register_then_task and register_else_task of IfThenElseCaptureManager at the end of the execution of the functor / lambda associated with the if section.

create_work_while

Introduction

A second method to create deferred work that is executed only if certain conditions are met is to use create_work_while. This function is defined in darma/interface/app/create_work_while.h and its signature is:

template <typename Functor, typename... Args>
auto create_work_while(Args&& ... args)

The actions triggered by a call to create_work_while are very similar to those initiated by create_work_if. create_work_while can be invoked using either a functor or a lambda: when a functor is used, its type should be specified as a template parameter of the function call. create_work_while creates and returns an object of type _create_work_while_helper, which is a struct specialized for functors and lambda that is defined in darma/impl/create_work/create_work_while_functor.h (for the functor case) and darma/impl/create_work/create_work_while_lambda.h (for the lambda case). The constructor of this object only stores the forwarded pack of arguments passed to create_work_while in args_fwd_tup, a std::tuple<...> member variable. Similarly to _create_work_if_helper, _create_work_while_helper has a public method do_ that is used to specify the conditional deferred work to be performed should the while clause be satisfied. This method creates an object of type _create_work_while_do_helper, which is described in the next section.

The _create_work_while_do_helper struct

_create_work_while_do_helper is an helper struct whose functionalities are similar to those of _create_work_then_helper: however, because there is never an additional clause as in the if-then-else case, the responsibilities of creating the capture manager object, initiating the dependency capture process and registering the task with the runtime are now assigned to the struct constructor instead of the destructor. As mentioned in the previous section, _create_work_while_do_helper creates an object of type WhileDoCaptureManager and then calls its method register_while_task. WhileDoCaptureManager is described in details in the next section.

The WhileDoCaptureManager struct

WhileDoCaptureManager is again very similar to IfThenElseCaptureManager. This struct is defined in darma/impl/create_work/create_work_while.h and is responsible for the dependency capture and tasks creation. The constructor of WhileDoCaptureManager creates two objects of type WhileFunctorTask and DoFunctorTask that are stored in member variables. These objects, in turn, inherit from FunctorTask (LambdaTask for lambda callables), and their instantiation will trigger the dependency capture mechanism. Similarly to the create_work_if case, dependency capture is performed in two steps: initially, the source and continuing access handles are stored in the std::map member variables while_task_ and do_task_, and their registration with the runtime is done only at a later time, just before the task is registered with the runtime. For the initial iteration of the while construct, this is done through a call to method register_while_task of WhileDoCaptureManager. On subsequent iterations, the registration of both the do and the following while tasks are done within method run of WhileFunctorTask, which is invoked to execute the conditions to be checked in deciding whether to execute the deferred conditional work or not specified in the do section of the construct.