In the task/channel programming model of Part I, the concepts of locality and concurrency are linked: a task is both a separate address space and a thread of control. In CC++ , these two concepts are separated. Processor objects represent address spaces, and threads represent threads of control. Processor objects can exist independently of threads, and more than one thread can be mapped to a processor object.
A processor object is defined by a C++ class declaration modified by the keyword global. A processor object is identical to a normal C++ class definition in all but two respects:
Processor object types can be inherited, and the usual C++ protection mechanisms apply, so private functions and data are accessible only from a processor object's member functions or from the member functions of derived objects. Hence, it is the member functions and data declared public that represent the processor object's interface.
For example, the following code from Program 5.3 creates a processor object class Construction with public member functions foundry and bridge. The class ChannelUser is specified as a base class and provides access to channel operations (Section 5.11).
global class Construction : public ChannelUser { public: void foundry(Channel, int); void bridge(Channel); };
A processor object is a unit of locality, that is, an address space within which data accesses are regarded as local and hence cheap. A thread executing in a processor object can access data structures defined or allocated within that processor object directly, by using ordinary C++ pointers.
Processor objects are linked together using global pointers. A global pointer is like an ordinary C++ pointer except that it can refer to other processor objects or to data structures contained within other processor objects. It represents data that are potentially nonlocal and hence more expensive to access than data referenced by ordinary C++ pointers.
A global pointer is distinguished by the keyword global. For example:
float *global gpf; // global pointer to a float char * *global gppc; // global pointer to pointer of type char C *global gpC; // global pointer to an object of type C
When the new statement is used to create an instance of a processor object, it returns a global pointer. For example, the statement
Construction *global foundry_pobj = new Construction;
from Program 5.3 creates a new processor object of type Construction and defines foundry_pobj to be a pointer to that object.
By default, a CC++ thread executes in the same processor object as its parent. Computation is placed in another processor object via an RPC. A thread needs only a global pointer to another processor object to be able to invoke any of its public member functions. For example, in the following line from Program 5.3, bridge_pobj is a global pointer to the processor object on which the consumer is to execute, and bridge is a public member function of that object.
bridge_pobj->bridge();
Remote procedure calls are discussed in more detail in Section 5.5 below.
A single thread executing in a processor object implements what in Part I we termed a task. Many CC++ programs create exactly one thread per processor object, yielding computation structures like those described in Part I. We discuss situations in which it is useful to create more than one thread per processor object in Section 5.7.
Program 5.4 uses processor objects and the par construct to implement a prototypical tree-structured computation. The program explores a binary tree recursively in the manner of Algorithm 1.1, creating a task (processor object + thread) for each tree node and returning the total number of leaf nodes that represent solutions. Notice the use of a parallel block to create the threads that search the two subtrees rooted at a nonleaf node. In this simple program, the tree is not represented by an explicit data structure; instead, a process's position in the tree is represented by an integer.
© Copyright 1995 by Ian Foster