![]() |
Home | Libraries | People | FAQ | More |
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_.
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() );
}
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.
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.
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.
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;
}
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
}
| Copyright © 2006 Matthew Calabrese |