Components and actions#

The accumulator examples demonstrate the use of components. Components are C++ classes that expose methods as a type of HPX action. These actions are called component actions. There are three examples: - accumulator - template accumulator - template function accumulator

Components are globally named, meaning that a component action can be called remotely (e.g., from another machine). There are two accumulator examples in HPX.

In the Asynchronous execution with actions and the Remote execution with actions, we introduced plain actions, which wrapped global functions. The target of a plain action is an identifier which refers to a particular machine involved in the computation. For plain actions, the target is the machine where the action will be executed.

Component actions, however, do not target machines. Instead, they target component instances. The instance may live on the machine that we’ve invoked the component action from, or it may live on another machine.

The components in these examples expose three different functions:

  • reset() - Resets the accumulator value to 0.

  • add(arg) - Adds arg to the accumulators value.

  • query() - Queries the value of the accumulator.

These examples create an instance of the (template or template function) accumulator, and then allow the user to enter commands at a prompt, which subsequently invoke actions on the accumulator instance.

Accumulator#

Setup#

The source code for this example can be found here: accumulator_client.cpp.

To compile this program, go to your HPX build directory (see Building HPX for information on configuring and building HPX) and enter:

$ make examples.accumulators.accumulator

To run the program type:

$ ./bin/accumulator_client

Once the program starts running, it will print the following prompt and then wait for input. An example session is given below:

commands: reset, add [amount], query, help, quit
> add 5
> add 10
> query
15
> add 2
> query
17
> reset
> add 1
> query
1
> quit

Walkthrough#

Now, let’s take a look at the source code of the accumulator example. This example consists of two parts: an HPX component library (a library that exposes an HPX component) and a client application which uses the library. This walkthrough will cover the HPX component library. The code for the client application can be found here: accumulator_client.cpp.

An HPX component is represented by two C++ classes:

  • A server class - The implementation of the component’s functionality.

  • A client class - A high-level interface that acts as a proxy for an instance of the component.

Typically, these two classes both have the same name, but the server class usually lives in different sub-namespaces (server). For example, the full names of the two classes in accumulator are:

  • examples::server::accumulator (server class)

  • examples::accumulator (client class)

The server class#

The following code is from server/accumulator.hpp.

All HPX component server classes must inherit publicly from the HPX component base class: hpx::components::component_base

The accumulator component inherits from hpx::components::locking_hook. This allows the runtime system to ensure that all action invocations are serialized. That means that the system ensures that no two actions are invoked at the same time on a given component instance. This makes the component thread safe and no additional locking has to be implemented by the user. Moreover, an accumulator component is a component because it also inherits from hpx::components::component_base (the template argument passed to locking_hook is used as its base class). The following snippet shows the corresponding code:

    class accumulator
      : public hpx::components::locking_hook<
            hpx::components::component_base<accumulator>>

Our accumulator class will need a data member to store its value in, so let’s declare a data member:

        argument_type value_;

The constructor for this class simply initializes value_ to 0:

        accumulator()
          : value_(0)
        {
        }

Next, let’s look at the three methods of this component that we will be exposing as component actions:

Here are the action types. These types wrap the methods we’re exposing. The wrapping technique is very similar to the one used in the Asynchronous execution with actions and the Remote execution with actions:

        HPX_DEFINE_COMPONENT_ACTION(accumulator, reset)
        HPX_DEFINE_COMPONENT_ACTION(accumulator, add)
        HPX_DEFINE_COMPONENT_ACTION(accumulator, query)

The last piece of code in the server class header is the declaration of the action type registration code:

HPX_REGISTER_ACTION_DECLARATION(
    examples::server::accumulator::reset_action, accumulator_reset_action)

HPX_REGISTER_ACTION_DECLARATION(
    examples::server::accumulator::add_action, accumulator_add_action)

HPX_REGISTER_ACTION_DECLARATION(
    examples::server::accumulator::query_action, accumulator_query_action)

Note

The code above must be placed in the global namespace.

The rest of the registration code is in accumulator.cpp

///////////////////////////////////////////////////////////////////////////////
// Add factory registration functionality.
HPX_REGISTER_COMPONENT_MODULE()

///////////////////////////////////////////////////////////////////////////////
typedef hpx::components::component<examples::server::accumulator>
    accumulator_type;

HPX_REGISTER_COMPONENT(accumulator_type, accumulator)

///////////////////////////////////////////////////////////////////////////////
// Serialization support for accumulator actions.
HPX_REGISTER_ACTION(
    accumulator_type::wrapped_type::reset_action, accumulator_reset_action)
HPX_REGISTER_ACTION(
    accumulator_type::wrapped_type::add_action, accumulator_add_action)
HPX_REGISTER_ACTION(
    accumulator_type::wrapped_type::query_action, accumulator_query_action)

Note

The code above must be placed in the global namespace.

The client class#

The following code is from accumulator.hpp

The client class is the primary interface to a component instance. Client classes are used to create components:

// Create a component on this locality.
examples::accumulator c = hpx::new_<examples::accumulator>(hpx::find_here());

and to invoke component actions:

c.add(hpx::launch::apply, 4);

Clients, like servers, need to inherit from a base class, this time, hpx::components::client_base:

    class accumulator
      : public hpx::components::client_base<accumulator, server::accumulator>

For readability, we typedef the base class like so:

        typedef hpx::components::client_base<accumulator, server::accumulator>
            base_type;

Here are examples of how to expose actions through a client class:

There are a few different ways of invoking actions:

  • Non-blocking: For actions that don’t have return types, or when we do not care about the result of an action, we can invoke the action using fire-and-forget semantics. This means that once we have asked HPX to compute the action, we forget about it completely and continue with our computation. We use hpx::post to invoke an action in a non-blocking fashion.

        void reset(hpx::launch::apply_policy)
        {
            HPX_ASSERT(this->get_id());

            typedef server::accumulator::reset_action action_type;
            hpx::post(action_type(), this->get_id());
        }
        hpx::future<argument_type> query(hpx::launch::async_policy)
        {
            HPX_ASSERT(this->get_id());

            typedef server::accumulator::query_action action_type;
            return hpx::async(action_type(), this->get_id());
        }
  • Synchronous: To invoke an action in a fully synchronous manner, we can simply call hpx::sync which is semantically equivalent to hpx::async().get() (i.e., create a future and immediately wait on it to be ready). Here’s an example from the accumulator client class:

        void add(argument_type arg)
        {
            HPX_ASSERT(this->get_id());

            typedef server::accumulator::add_action action_type;
            action_type()(this->get_id(), arg);
        }

Note that this->get_id() references a data member of the hpx::components::client_base base class which identifies the server accumulator instance.

hpx::naming::id_type is a type which represents a global identifier in HPX. This type specifies the target of an action. This is the type that is returned by hpx::find_here in which case it represents the locality the code is running on.

Template accumulator#

Walkthrough#

The server class#

The following code is from server/template_accumulator.hpp.

Similarly to the accumulator example, the component server class inherits publicly from hpx::components::component_base and from hpx::components::locking_hook ensuring thread-safe method invocations.

    template <typename T>
    class template_accumulator
      : public hpx::components::locking_hook<
            hpx::components::component_base<template_accumulator<T>>>

The body of the template accumulator class remains mainly the same as the accumulator with the difference that it uses templates in the data types.

        typedef T argument_type;

        template_accumulator()
          : value_(0)
        {
        }

        ///////////////////////////////////////////////////////////////////////
        // Exposed functionality of this component.

        /// Reset the components value to 0.
        void reset()
        {
            //  set value_ to 0.
            value_ = 0;
        }

        /// Add the given number to the accumulator.
        void add(argument_type arg)
        {
            //  add value_ to arg, and store the result in value_.
            value_ += arg;
        }

        /// Return the current value to the caller.
        argument_type query() const
        {
            // Get the value of value_.
            return value_;
        }

        ///////////////////////////////////////////////////////////////////////
        // Each of the exposed functions needs to be encapsulated into an
        // action type, generating all required boilerplate code for threads,
        // serialization, etc.
        HPX_DEFINE_COMPONENT_ACTION(template_accumulator, reset)
        HPX_DEFINE_COMPONENT_ACTION(template_accumulator, add)
        HPX_DEFINE_COMPONENT_ACTION(template_accumulator, query)

The last piece of code in the server class header is the declaration of the action type registration code. REGISTER_TEMPLATE_ACCUMULATOR_DECLARATION(type) declares actions for the specified type, while REGISTER_TEMPLATE_ACCUMULATOR(type) registers the actions and the component for the specified type, using macros to handle boilerplate code.

#define REGISTER_TEMPLATE_ACCUMULATOR_DECLARATION(type)                        \
    HPX_REGISTER_ACTION_DECLARATION(                                           \
        examples::server::template_accumulator<type>::reset_action,            \
        HPX_PP_CAT(__template_accumulator_reset_action_, type))                \
                                                                               \
    HPX_REGISTER_ACTION_DECLARATION(                                           \
        examples::server::template_accumulator<type>::add_action,              \
        HPX_PP_CAT(__template_accumulator_add_action_, type))                  \
                                                                               \
    HPX_REGISTER_ACTION_DECLARATION(                                           \
        examples::server::template_accumulator<type>::query_action,            \
        HPX_PP_CAT(__template_accumulator_query_action_, type))                \
    /**/

#define REGISTER_TEMPLATE_ACCUMULATOR(type)                                    \
    HPX_REGISTER_ACTION(                                                       \
        examples::server::template_accumulator<type>::reset_action,            \
        HPX_PP_CAT(__template_accumulator_reset_action_, type))                \
                                                                               \
    HPX_REGISTER_ACTION(                                                       \
        examples::server::template_accumulator<type>::add_action,              \
        HPX_PP_CAT(__template_accumulator_add_action_, type))                  \
                                                                               \
    HPX_REGISTER_ACTION(                                                       \
        examples::server::template_accumulator<type>::query_action,            \
        HPX_PP_CAT(__template_accumulator_query_action_, type))                \
                                                                               \
    typedef ::hpx::components::component<                                      \
        examples::server::template_accumulator<type>>                          \
        HPX_PP_CAT(__template_accumulator_, type);                             \
    HPX_REGISTER_COMPONENT(HPX_PP_CAT(__template_accumulator_, type))          \
    /**/

Note

The code above must be placed in the global namespace.

Finally, HPX_REGISTER_COMPONENT_MODULE() in file server/template_accumulator.cpp adds the factory registration functionality.

The client class#

The client class of the template accumulator can be found in template_accumulator.hpp and is very similar to the client class of the accumulator with the only difference that it uses templates and hence can work with different types.

Template function accumulator#

Walkthrough#

The server class#

The following code is from server/template_function_accumulator.hpp.

The component server class inherits publicly from hpx::components::component_base.

    class template_function_accumulator
      : public hpx::components::component_base<template_function_accumulator>

typedef hpx::spinlock mutex_type defines a mutex_type as hpx::spinlock for thread safety, while the code that follows exposes the functionality of this component.

        ///////////////////////////////////////////////////////////////////////
        // Exposed functionality of this component.

        /// Reset the value to 0.
        void reset()
        {
            // Atomically set value_ to 0.
            std::lock_guard<mutex_type> l(mtx_);
            value_ = 0;
        }

        /// Add the given number to the accumulator.
        template <typename T>
        void add(T arg)
        {
            // Atomically add value_ to arg, and store the result in value_.
            std::lock_guard<mutex_type> l(mtx_);
            value_ += static_cast<double>(arg);
        }

        /// Return the current value to the caller.
        double query() const
        {
            // Get the value of value_.
            std::lock_guard<mutex_type> l(mtx_);
            return value_;
        }
  • reset(): Resets the accumulator value to 0 in a thread-safe manner using std::lock_guard.

  • add(): Adds a value to the accumulator, allowing any type T that can be cast to double.

  • query(): Returns the current value of the accumulator in a thread-safe manner.

To define the actions for reset() and query() we can use the macro HPX_DEFINE_COMPONENT_ACTION. However, actions with template arguments require special type definitions. Therefore, we use make_action() to define add().

        ///////////////////////////////////////////////////////////////////////
        // Each of the exposed functions needs to be encapsulated into an
        // action type, generating all required boilerplate code for threads,
        // serialization, etc.

        HPX_DEFINE_COMPONENT_ACTION(template_function_accumulator, reset)
        HPX_DEFINE_COMPONENT_ACTION(template_function_accumulator, query)

        // Actions with template arguments (see add<>() above) require special
        // type definitions. The simplest way to define such an action type is
        // by deriving from the HPX facility make_action.
        template <typename T>
        struct add_action
          : hpx::actions::make_action<void (template_function_accumulator::*)(
                                          T),
                &template_function_accumulator::template add<T>,
                add_action<T>>::type
        {
        };

The last piece of code in the server class header is the action registration:

HPX_REGISTER_ACTION_DECLARATION(
    examples::server::template_function_accumulator::reset_action,
    managed_accumulator_reset_action)

HPX_REGISTER_ACTION_DECLARATION(
    examples::server::template_function_accumulator::query_action,
    managed_accumulator_query_action)

Note

The code above must be placed in the global namespace.

The rest of the registration code is in accumulator.cpp

///////////////////////////////////////////////////////////////////////////////
// Add factory registration functionality.
HPX_REGISTER_COMPONENT_MODULE()

///////////////////////////////////////////////////////////////////////////////
typedef hpx::components::component<
    examples::server::template_function_accumulator>
    accumulator_type;

HPX_REGISTER_COMPONENT(accumulator_type, template_function_accumulator)

///////////////////////////////////////////////////////////////////////////////
// Serialization support for managed_accumulator actions.
HPX_REGISTER_ACTION(accumulator_type::wrapped_type::reset_action,
    managed_accumulator_reset_action)
HPX_REGISTER_ACTION(accumulator_type::wrapped_type::query_action,
    managed_accumulator_query_action)

Note

The code above must be placed in the global namespace.

The client class#

The client class of the template accumulator can be found in template_function_accumulator.hpp and is very similar to the client class of the accumulator with the only difference that it uses templates and hence can work with different types.