Components and actions: Accumulator¶
The accumulator example demonstrates the use of components. Components are C++ classes that expose methods as a type of HPX action. These actions are called component actions.
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; accumulator.
In the Asynchronous execution with hpx::async and actions: Fibonacci and the Remote execution with actions: Hello world, 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 component in this example exposes three different functions:
reset()
- Resets the accumulator value to 0.add(arg)
- Addsarg
to the accumulators value.query()
- Queries the value of the accumulator.
This example creates an instance of the accumulator, and then allows the user to enter commands at a prompt, which subsequently invoke actions on the accumulator instance.
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 HPX build system 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 components functionality.
- A client class - A high-level interface that acts as a proxy for an instance of the component.
Typically, these two classes all 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: 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, 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:
/// 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_;
}
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 hpx::async and actions: Fibonacci and the Remote execution with actions: Hello world:
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 which 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::apply
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::apply<action_type>(this->get_id());
}
- Asynchronous: Futures, as demonstrated in Asynchronous execution with hpx::async: Fibonacci, Asynchronous execution with hpx::async and actions: Fibonacci, and the Remote execution with actions: Hello world, enable asynchronous action invocation. Here’s an example from the accumulator client class:
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>(hpx::launch::async, this->get_id());
}
- Synchronous: To invoke an action in a fully synchronous manner, we can
simply call
hpx::async
().get()
(e.g., 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.