![]() |
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; managed_accumulator and simple_accumulator (we will talk more about the differences between the two later). This tutorial will examine the managed_accumulator variant.
In the Fibonacci Example and the Hello World Example, 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)
- Adds arg
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.
The source code for this example can be found here: accumulators.
To compile this program, go to your HPX build directory (see Getting Started for information on configuring and building HPX) and enter:
make examples.accumulator.managed_accumulator
To run the program type:
./bin/managed_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
Now, let's take a look at the source code of the managed_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: managed_accumulator_client.cpp.
An HPX component is represented by three C++ classes:
Typically, these three classes all have the same name, but stubs and server
classes usually live in different sub-namespaces (server
and stubs
respectively).
For example, the full names of the three classes in managed_accumulator
are:
examples::server::managed_accumulator
(server class)
examples::stubs::managed_accumulator
(stubs class)
examples::managed_accumulator
(client class)
The following code is from: server/managed_accumulator.hpp.
All HPX component server classes must inherit publicly from an HPX component base class. There are currently two component base classes:
hpx::components::managed_component_base
<>
- Managed components are components
which are allocated in bulk by HPX. Managed components
are more efficient if you are creating a large number (e.g. hundreds
or more per machine) of component instances.
hpx::components::simple_component_base
<>
- Simple components are components
which are allocated individually by HPX. Simple
components are more efficient if you are creating a small number (e.g.
only a handful per machine) of component instances.
The managed_accumulator component inherits from hpx::components::locking_hock
<>
. 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, managed_accumulator
component is a managed component, because it also inherits from hpx::components::managed_component_base
<>
(the template argument passed to
locking_hook is used as its base class). The following snippet shows the
corresponding code:
class managed_accumulator : public hpx::components::locking_hook< hpx::components::managed_component_base<managed_accumulator> >
Our accumulator class will need a data member to store its value in, so let's declare a data member:
private: argument_type value_;
The constructor for this class simply initializes value_
to 0:
managed_accumulator() : value_(0) {}
Next, let's look at the three methods of this component that we will be exposing as component actions:
/// Reset the 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 Fibonacci Example and the Hello World Example:
HPX_DEFINE_COMPONENT_ACTION(managed_accumulator, reset); HPX_DEFINE_COMPONENT_ACTION(managed_accumulator, add); HPX_DEFINE_COMPONENT_CONST_ACTION(managed_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::managed_accumulator::reset_action, managed_accumulator_reset_action); HPX_REGISTER_ACTION_DECLARATION( examples::server::managed_accumulator::add_action, managed_accumulator_add_action); HPX_REGISTER_ACTION_DECLARATION( examples::server::managed_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 managed_accumulator.cpp.
/////////////////////////////////////////////////////////////////////////////// // Add factory registration functionality. HPX_REGISTER_COMPONENT_MODULE(); /////////////////////////////////////////////////////////////////////////////// typedef hpx::components::managed_component< examples::server::managed_accumulator > accumulator_type; HPX_REGISTER_MINIMAL_COMPONENT_FACTORY(accumulator_type, managed_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::add_action, managed_accumulator_add_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 following code is from stubs/managed_accumulator.hpp.
All stubs classes must inherit from the stubs base class, hpx::components::stub_base
<>
:
struct managed_accumulator : hpx::components::stub_base<server::managed_accumulator>
The stubs class contains helper functions which invoke actions on component instances. There are a few different ways of invoking actions:
hpx::applier::apply
<>()
instead of hpx::lcos::async
<>()
to invoke an action in a
non-blocking fashion. Here's an example from the managed_accumulator
stubs class:
static void reset_non_blocking(hpx::naming::id_type const& gid) { typedef server::managed_accumulator::reset_action action_type; hpx::apply<action_type>(gid); }
static hpx::lcos::future<argument_type> query_async(hpx::naming::id_type const& gid) { typedef server::managed_accumulator::query_action action_type; return hpx::async<action_type>(gid); }
hpx::lcos::async
<>().get()
(e.g., create a future and immediately
wait on it to be ready). Here's an example from the managed_accumulator
stubs class:
static void add_sync(hpx::naming::id_type const& gid, argument_type arg) { typedef server::managed_accumulator::add_action action_type; hpx::async<action_type>(gid, arg).get(); }
hpx::naming::id_type
is a type which represents a global identifier in HPX.
This is the type that is returned by hpx::find_here
()
. This type specifies the target of an
action.
The following code is from managed_accumulator.hpp.
The client class is the primary interface to a component instance. Client classes are used to create components:
examples::managed_accumulator c; c.create(hpx::find_here()); // Create a component on this machine.
and to invoke component actions:
c.add_sync(4);
Clients, like stubs and servers, need to inherit from a base class, this
time, hpx::components::client_base
<>
:
class managed_accumulator : public hpx::components::client_base< managed_accumulator, stubs::managed_accumulator >
For readability, we typedef the base class like so:
typedef hpx::components::client_base< managed_accumulator, stubs::managed_accumulator > base_type;
Here are examples of how to expose actions through a client class:
void reset_non_blocking() { HPX_ASSERT(this->get_gid()); this->base_type::reset_non_blocking(this->get_gid()); }
hpx::lcos::future<argument_type> query_async() { HPX_ASSERT(this->get_gid()); return this->base_type::query_async(this->get_gid()); }
void add_sync(argument_type arg) { HPX_ASSERT(this->get_gid()); this->base_type::add_sync(this->get_gid(), arg); }
Note that this->gid_
references a data member of the
hpx::components::client_base
<>
base class.