boost.png (6897 bytes) Home Libraries People FAQ More

PrevUpHomeNext

Basic Usage

Parallel copy
Parallel for_each
Creating your own parallelable algorithms
Signaling an action
Instantiating atomic types
Instantiating active types
Accessing active objects via actions
Creating active interfaces
Using type traits

Parallel copy

One operation which is easily parallelable is an algorithm which copies the objects represented in one range to another range. Much like ::std::copy in the standard library, the corresponding algorithm in Surge.Act takes a source iterator range, represented by a begin and end iterator, followed by the begin iterator of a target range. Use of the algorithm is similar, only it exists in the ::surge::act namespace as opposed to the ::std namespace, and it also has the benefit of being useable with Boost.Lambda.

The following code uses Surge.Act to copy one range to another, making the call parallel if the concurrent act model is the current default and the compiler supports OpenMP.


#include <surge/act/algorithm.hpp>

#include <deque>
#include <vector>

int main()
{
  using std::vector;
  using std::deque;
  using surge::act::copy;

  // Source range of 100 elements, each having the value 5
  vector< int > source( 100, 5 );

  //Target range
  deque< long > target( 100 );

  // Uses ::surge::act::copy to copy from source to target
  copy( source.begin(), source.end(), target.begin() );
}

Note that with user-defined types, an additional step has to be taken to allow the algorithm to execute in parallel. For more details, see __paralleltraits_.

Parallel for_each

Another common algorithm which is often easily parallelable is for_each. Just like with copy, the semantics of for_each are similar to those of for_each in the STL. Here, the first two arguments are the begin and end iterators of a desired range, and the last is a function object to be applied on each of the elements in that range. This code also introduces a new mechanism, in the form of a base type called parallel_safe, which specifies that the given function object may be copied any number of times with each copy able to be invoked at the same time in different threads of execution and have the effect be the same as if a single instance were used in one thread for all iterations.

Without inheriting from the parallel_safe base type or using other means to identify your function as being safe for use in a parallel algorithm described in the __paralleltraits_ section, calling for_each or any other parallelable algorithm provided by Surge.Act with an instance of your function object type will cause the algorithm to run synchronously.

#include <surge/act/algorithm.hpp>
#include <surge/act/parallel_safe.hpp>

#include <vector>

struct increment
  : surge::act::parallel_safe
{
  void operator ()( int& target ) const
  {
    ++target;
  }
};

int main()
{
  using std::vector;
  using surge::act::for_each;

  // Source range of 100 elements, each having the value 4
  vector< int > source( 100, 4 );

  // Uses ::surge::act::for_each to increment each element
  for_each( source.begin(), source.end(), increment() );
}

Creating your own parallelable algorithms

While Surge.Act provides many common parrallelable algorithms, it would not be complete without the ability for users to create their own. The creation of such algorithms in Surge.Act is performed through the use of the function object basic_for. basic_for is a simplified form of for loop construct which limits the loop variable to built-in integral types, limits the comparison to ordered comparison operators, and limits the step expression to additive operations applied to the loop variable. All values other than the loop variable are calculated once upon entry into the algorithm. If an attempt is made to specify more complex expressions than described, the code will not compile.

In order to use the basic_for algorithm, you may #include <surge/act/algorithm/basic_for.hpp> or #include <surge/act/algorithm.hpp>. Initialization, condition, and step expressions are specified as the first, second, and third arguments respectively, using surge::act::for_var to refer to the loop variable. The type of the loop variable is determined by the type of the variable of which it is initialized to. To represent the body of the for loop, one must pass a function object as an argument to the index operator following the call to basic_for, or as a fourth argument to basic_for. Function objects passed in this manner are applied during each iteration of the associated basic_for operation and receive the current loop variable value as an argument with each call.


#include <surge/act/algorithm.hpp>

#include <iostream>

struct output_for_var
  : surge::act::parallel_safe
{
  void operator ()( int index ) const
  {
    std::cout << index;
  }
};

int main()
{
  using surge::act::basic_for;
  using surge::act::for_var;
  
  basic_for( for_var = 0, for_var < 10, ++for_var )
  [
    output_for_var()
  ];
}

In future releases, Surge.Act will expose more constructs other than basic_for.

Signaling an action

Having algorithms run in parallel and join prior to returning is one easy way to take advantantage of multi-threading capabilities. However, as previously described, often times one may wish to signal an operation to be performed and not require the actual operation to complete its execution until some later point in code. This allows you to signal one task, perform some other unrelated operations, and then come back at some later point in time to optionally force a wait for the original operation to complete. Such behavior is expressible in Surge.Act through the use of actions.

In the following code, an action is used to signal a function call using the default act model, perform some simple calculations on unrelated data, and then finally go back and obtain the result of the operation.


#include <surge/act/action.hpp>

#include <iostream>

unsigned int factorial( unsigned int value )
{
  return ( value == 0 ) ? 1 : value * factorial( value - 1 );
}

int main()
{
  using surge::act::action;
  using surge::act::as_function;
  using std::cout;
  using std::endl

  // Where unsigned int is the return type of the function
  action< unsigned int > fac_10( as_function( factorial ), 10 );

  factorial( 15 );

  // Wait for the return value and store it in result
  int const result = fac_10->inactive_value();

  cout << "10! = " << result << endl;
}

The above code presents some unfamiliar functions. First, we have as_function, which is used to specify that the first argument you are passing to the action constructor is a function to execute. This is necessary for disambiguation between other action constructors which will be described later. Following that argument is the value 10, which is the argument to be passed to factorial for invocation. Finally, before main finishes execution, we indirectly call inactive_value through fac_10, which is how we specify that we wish to obtain a copy of the result of the function call, implicitly waiting for the function to complete. In this example, a function was used, however, a function object could have been used as well.

Instantiating atomic types

Instantiating active types

Active objects in Surge.Act are implemented through the use of a pseudo-qualifier applied via a macro. Much like const or volatile, you can take any type and add the qualifier to it, which limits the interface of the type to only active qualified member functions and other functions which take an active qualified version as a parameter. Like with const or volatile member functions, you must specify in their definition that the object must be appropriately qualified for use with the function. Such calls return actions so that you may wait for completion and access the result of the call similar to the manner in which you would for active function calls as described in the previous section. Surge.Act defines appropriate operations for built-ins such that you may, for example, work with active arithmetic types intuitively.

The simplest way to qualify a type with active is to #include <surge/act/active.hpp> and use the macro SURGE_ACTIVE, which takes a single type as a parameter enclosed in parenthesis yields a datatype which has the same const-qualification as the type which was passed and which represents the active form of the type.

The following code defines an active int and performs a series of operations on it.


#include <surge/act/action.hpp>
#include <surge/act/active.hpp>

#include <iostream>

int main()
{
  using surge::act::action;
  using std::cout;
  using std::endl;

  // Note that int is encapsulated in an extra set of parenthesis
  SURGE_ACTIVE((int)) value = 0;

  value += 10;

  value = -value;

  action< int > result = value + value;

  ++value;

  // result's value may not be calculated at this point

  // Output the result (forcing a wait)
  cout << "value after the calculation is completed: "
       << result->inactive_value() << endl;
}

The above code creates an active int and signals several operations to be performed using its value. While each operation is guaranteed to be performed in order, it's dependent on the default act model as to when they will be run. At the end of main, the application waits for the result, much like the code in the previous section. Here, the wait also implies that the other calculations on value signaled prior to result's initialization are completed by the time the call to inactive_value returns. The call to ++value, however, may have not yet occured.

Accessing active objects via actions

As was mentioned, the results of actions can be accessed indirectly through action objects. This allows you to make active function calls and signal operations on the result without having to wait for the initial function to complete.


#include <surge/act/action.hpp>

#include <iostream>

unsigned int factorial( unsigned int value )
{
  return ( value == 0 ) ? 1 : value * factorial( value - 1 );
}

int main()
{
  using surge::act::action;
  using surge::act::as_function;
  using std::cout;
  using std::endl;

  // Where int is the return type of the function
  action< unsigned int > fac_10( as_function( factorial ), 10 );

  unsigned int const value = factorial( 15 );

  // Use -> and * to access the result value as though it were an active
  // object. Note that this does not a force a wait, but rather, it adds the
  // += operation to a queue
  action< unsigned int > result = (*fac_10) += value;

  // fac_10's value may not be calculated at this point

  // Forces a wait to get the resultant value
  cout << "10! + 15! = " << result->inactive_value() << endl;
}

Creating active interfaces

Working with built-in types is great, but without the ability to define active interfaces for your own types, Surge.Act would leave much to be desired. The code needed to make simple interfaces for active objects tends to be somewhat complicated, so a collection of macros are provided to make the development of active interfaces easier.

To create an active interface for a type, you must start by partially specializing active_interface in surge::act for your type. The body of the template specialization now corresponds to an extension of the body of the type for which you are making the active interface. Here you may use macros provided by Surge.Act to create member functions and friend functions which may be used with active qualified instantiations of your type. From within the defintion of these functions, you have access to the active-unqualified version of your object.

From that point on, you may use the functions you created when you instantiate your type with active qualification.


#include <surge/act/action.hpp>
#include <surge/act/active.hpp>

#include <iostream>

struct your_type
{
  your_type() : value( 0 ) {}
  int value;
};

namespace surge
{
namespace act
{

SURGE_ACT_ACTIVE_INTERFACE_SPEC( ::your_type )
{
  SURGE_ACT_MEM_FUN( (void), update_value, ((int),left) ((int),right) )
  {
    using ::std::cout;
    using ::std::endl;
    
    // Use target to access the target object
    target.value += left * right;
    cout << "New value: " << target.value << endl;
  }
};

}
}

int main()
{
  using surge::act::action;

  SURGE_ACTIVE((your_type)) object;
  
  // Call update value (queues function, returns immediately)
  object.update_value( 3, 4 );
  
  action<> const running_fun = object.update_value( 1, 2 );
  
  object.update_value( 1, 6 );
  
  running_fun.wait(); // Force running_fun to complete
}

Using type traits

Copyright © 2006 Matthew Calabrese

PrevUpHomeNext