6.2. The Command Pattern

The second pattern we will explore is the command pattern. The command pattern is simple; to implement it, an object needs to implement the following interface:

public interface Command {
  public void execute();
}

In other words, any object that implements the command pattern provides an execute method. What the execute method does depends on the context and application in which it is being used. The important point is, if you have an object that implements the command pattern, you know you can invoke its execute method. While this pattern is simple, its power stems from our ability to drop an object into a space and have another process pick it up and invoke methods on it, even if the remote process has never seen the object before. In other words, we can pass object behavior through spaces, as well as object data, which is a very powerful technique.

You might be asking yourself why this is useful. Let's reconsider our Mandelbrot example: our application required that we run a number of worker processes that wait for Mandelbrot tasks and compute them. But what if we want to compute Julia sets (a slightly different kind of fractal image), or ray trace images, or multiply matrices? Our Mandelbrot worker can't perform these new kinds of tasks. For each kind of computation, we'd have to create and run a new worker applet or application that knows how to service those kinds of tasks. It would be far more elegant (not to mention less work for us) to create one generic worker application that accepts task requests of many kinds, where the code to perform the task is bundled with the task object itself, in an execute method.

So in other words, using the command pattern, we can create a generic application that can service the requests of many master processes that need tasks computed. Suppose we have a network of fast graphics workstations that handle requests for ray-traced images from several workgroups working on animations. Using the command pattern, each group can supply its own specialized code to compute the images, while using the same servers to compute them. The workgroups only need to know and maintain one interface to the group of compute servers.

The command pattern isn't specific to space-based programming; in fact, it is a well known pattern that has been used in a variety of domains to allow an object to specify its own behavior in response to certain events. The command pattern has a wide range of uses, from implementing actions in user interface code, to providing “undo objects” that can undo state changes in a system. For more detailed information on this pattern, refer to Design Patterns: Elements of Reusable Object-Oriented Software, by Erich Gamma, et al.

6.2.1. Implementing a Compute Server

Let's implement a simple compute server based on the command pattern. The compute server consists of a collection of worker processes. Each worker continually takes a task from the space (which was placed there by some master process), computes it by calling its execute method, and then continues on to the next task. The tasks themselves can perform space operations; typically they write the results of the worker's computation into a result entry, as in the Mandelbrot example.

We'll start by defining a generic task entry class that implements the Command interface and that will serve as the base class for all other (more specialized) task entries:

public class TaskEntry implements Entry, Command {
  public ResultEntry execute() {
    throw new RuntimeException(
       "TaskEntry.execute() not implemented");
  }
}

The TaskEntry class provides an execute method that throws an exception if it isn't overridden by a subclass. Our reason for doing this is to force any subclass to implement this method. The conventional method of enforcing a subclass to implement a method is to make the class abstract, but we have avoided doing that here. If TaskEntry were defined to be abstract, then we would not be able to instantiate it to act as a template to retrieve entries belonging to any of its subclasses—a major drawback. Note that execute returns a ResultEntry object; we will see why shortly.

6.2.2. The Generic Worker

Now that we have a task entry, let's look at a generic worker that processes task entries. Here is the skeleton for the generic worker:

public class GenericWorker {
  JavaSpace space;

  public static void main(String[] args) {
    GenericWorker worker = new GenericWorker();
    worker.startWork();
  }

  public GenericWorker() {
    space = SpaceAccessor.getSpace();
  }

  public void startWork() {
    TaskEntry taskTmpl = new TaskEntry();

    while (true) {
      System.out.println("getting task");
      try {
          TaskEntry task = (TaskEntry)
            space.take(taskTmpl, null, Lease.FOREVER);
          ResultEntry result = task.execute();
          if (result != null) {
             space.write(result, null, Lease.FOREVER);
          }
      } catch (RemoteException e) {
         System.out.println(e);
      } catch (TransactionException e) {
         System.out.println(e);
      } catch (UnusableEntryException e) {
         System.out.println(e);
      } catch (InterruptedException e) {
         System.out.println("Task cancelled");
      }
    }
  }
}

The worker runs as an application and executes the main method. The method first instantiates a GenericWorker object; the constructor simply obtains a reference to a space object. Then main calls the worker's startWork method.

The startWork method first creates a task entry to be used as a template to match any available task entry in the space. Then the method enters a loop, in which it waits for a task entry in the space. When one is available, a task is taken from the space and assigned to the variable task, and its execute method is called. Note that the worker process doesn't need to know what kind of task it has picked up or the details of how to compute it—it just calls the task's execute method. If successful, execute returns a ResultEntry object, which is then written into the space. In this manner, the results of a task computation are communicated back to the process that created the task.

A ResultEntry is an object that implements the Entry interface. Once again, we make it a class rather than an abstract class or interface, because we want to be able to use it as a template:

public class ResultEntry implements Entry {
}

6.2.3. The Generic Master

The job of the master process is to generate tasks—which will be computed by workers monitoring the space—and then to collect the results. First we'll create an abstract generic master class:

public abstract class GenericMaster extends Applet
  implements Runnable {

  private JavaSpace space;
  private Thread thread;

  public void init() {
    space = SpaceAccessor.getSpace();

    thread = new Thread(this);
    thread.start();
  }

  public synchronized void run() {
    generateTasks();
    collectResults();
  }

  protected abstract void generateTasks();

  protected abstract void collectResults();

  protected void writeTask(TaskEntry task) {
    try {
        space.write(task, null, Lease.FOREVER);
    } catch (RemoteException e) {
       e.printStackTrace();
    } catch (TransactionException e) {
       e.printStackTrace();
    }
  }

  protected ResultEntry takeResult(ResultEntry template) {
    try {
       ResultEntry result = (ResultEntry)
         space.take(template, null, Long.MAX_VALUE);
       return result;
    } catch (RemoteException e) {
       e.printStackTrace();
    } catch (TransactionException e) {
       e.printStackTrace();
    } catch (UnusableEntryException e) {
       e.printStackTrace();
    } catch (InterruptedException e) {
       System.out.println("Task cancelled");
    }
    return null;
  }
}

The GenericMaster is an applet; its init method first obtains a handle to a space object, and then instantiates and starts a new thread. The run method follows a familiar pattern: it calls the methods generateTasks and collectResults. These two abstract methods are meant to create a set of tasks for the workers and to retrieve a set of results.

In the GenericMaster we've also provided two convenience methods: one to write a task to the space, and another to take a result from the space.

6.2.4. Creating Specialized Tasks and Results

The TaskEntry, ResultEntry, GenericWorker, and GenericMaster classes make up the foundation of our compute server. Before we can use the compute server, though, we first need to subclass the TaskEntry and ResultEntry classes, and also to create a specialized master process that generates specific kinds of tasks and collects the results of computing them. Then we can start up the specialized master process and also start up as many instances of the worker on as many machines as we like, and our compute server will be in full gear.

To demonstrate the compute server, we are going to create two simple subclasses of TaskEntry and a master class that generates tasks from both subclasses. Rather than implementing complex, compute-intensive tasks, we'll create two simple (and quickly computed) subclasses that compute factorials and Fibonacci numbers. Our aim here is to demonstrate how the compute server works, not to concentrate on creating an application that shows good parallel performance (recall the discussion of the computation/communication ratio in Section 6.1). When we get to Chapter 11, we will create a more sophisticated compute server and explore its use in breaking password encryption, which is a highly compute-intensive problem.

Let's start with our factorial task. The factorial function is defined as follows:

factorial(0) = 1

factorial(n) = n * factorial(n-1)

So, given an integer n, we can compute its factorial by multiplying n * (n -1) * (n -2) * (n -3) * ... * 1. Here is a subclass of TaskEntry that implements the factorial task:

public class FactorialTask extends TaskEntry {
  public Integer n;

  public FactorialTask() {
  }

  public FactorialTask(int n) {
    this.n = new Integer(n);
  }

  public ResultEntry execute() {
    System.out.println(
      "Computing Factorial of " + n);
    int num = n.intValue();
    return new FactorialResult(num, factorial(num));
  }

  public int factorial(int i) {
    if (i == 0) {
       return 1;
    } else {
      return i * factorial(i - 1);
    }
  }
}

As you can see, FactorialTask is a subclass of TaskEntry. The class has one field, an integer n that indicates which factorial to compute, and the usual no-arg and convenience constructors. In addition, we have overridden the parent class's execute method. The new execute method converts the field n from an Integer into an int, and then passes the integer to the factorial method, which recursively computes the factorial and returns the final result.

Once the factorial is returned, the execute method creates and returns a FactorialResult object, which extends ResultEntry and has fields that hold the integer and its factorial:

public class FactorialResult extends ResultEntry {
  public Integer n;
  public Integer factorial;

  public FactorialResult() {
  }

  public FactorialResult(int n, int factorial) {
    this.n = new Integer(n);
    this.factorial = new Integer(factorial);
  }
}

The FactorialResult object is returned by a call to execute, and as a result will be written into the space by a worker process.

Our Fibonacci task is another simple mathematical task that computes a number in the Fibonacci sequence: 1 1 2 3 5 8 13 21 34 55 89.... The Fibonacci function is defined recursively as follows:

fibonacci(0) = 1

fibonacci(1) = 1

fibonacci(n) = fibonacci(n -2) + fibonacci(n -1)

In other words, a number in this sequence is computed by adding the previous two numbers in the sequence (for instance, 55 = 21 + 34). Here is the subclass of TaskEntry that implements our Fibonacci task:

public class FibonacciTask extends TaskEntry {
  public Integer n;

  public FibonacciTask() {
  }

  public FibonacciTask(int n) {
    this.n = new Integer(n);
}

  public ResultEntry execute() {
    System.out.println(
      "Computing Fibonacci of " + n);
    int num = n.intValue();
    return new FibonacciResult(num, fibonacci(num));
  }

  public int fibonacci(int n) {
    int lo = 1;
    int hi = 1;

    for (int i = 1; i < n; i++) {
      hi = lo + hi;
      lo = hi - lo;
    }
    return hi;
  }
}

As you can see, this task entry mirrors the factorial task, except that it computes a Fibonacci number and returns a FibonacciResult (which, as you can see in the complete source code, mirrors FactorialResult).

6.2.5. Creating a Specialized Master

We've already seen the code for our generic master process, but recall that the generateTasks and collectResults methods are abstract. We need to create a subclass of our generic master, with its own methods to generate specific tasks, and then collect the results of computing them.

Let's create a subclass to generate both factorial and Fibonacci tasks and retrieve their results:

public class FibFactMaster extends GenericMaster {
  public void generateTasks() {
    for (int i = 0; i < 10; i++) {
        FactorialTask task = new FactorialTask(i);
        writeTask(task);
    }

    for (int i = 0; i < 10; i++) {
        FibonacciTask task = new FibonacciTask(i);
        writeTask(task);
    }
  }

  public void collectResults() {
    ResultEntry template = new ResultEntry();
    for (int i = 0; i < 20; i++) {
        ResultEntry result = takeResult(template);

        if (FactorialResult.class.isInstance(result)) {
           FactorialResult fact = (FactorialResult)result;
           System.out.println("Factorial of " + fact.n +
             " is " + fact.factorial);
        } else if (FibonacciResult.class.isInstance(result)) {
          FibonacciResult fib = (FibonacciResult)result;
          System.out.println("Fibonacci of " +
            fib.n + " is " + fib.fibonacci);
        } else {
          System.out.println("Result class not found");
        }
    }
  }
}

Here in FibFactMaster we've subclassed the generateTasks and collectResults methods. The generateTasks method creates ten factorial tasks (to compute factorials for the numbers 0 through 9) and calls the writeTask convenience method to write the tasks to the space. Then ten Fibonacci tasks are created and written to the space.

The collectResults method first creates a ResultEntry template and then loops for twenty iterations (the number of result entries that will be computed and placed in the space). Each time through the loop, we take a result entry from the space. Note that we are using the generic ResultEntry to take a result from the space, rather than one of the derived classes FactorialResult or FibonacciResult—recall that by the semantics of template matching, the ResultEntry template can potentially match any subclass. Because the ResultEntry template has no defined fields, it will match any other ResultEntry object, or any subclassed objects.

Once we've retrieved an entry from the space, we then need to know what type of result it is, factorial or Fibonacci. We make use of the isInstance method (from the core Java API) to determine which it is. In either case, we print an appropriate result message.

6.2.6. Running the Compute Server

At this point, all that's left to do is run the example. Start up any number of workers and then a master process (or, thanks to loose coupling and persistence, feel free to run the master first and then the workers). The output of a worker process will look something like the following:

getting task
computing Fibonacci of 8
getting task
computing Fibonacci of 9
getting task
Computing Factorial of 2
getting task
Computing Factorial of 3
getting task
Computing Factorial of 4
getting task
Computing Factorial of 5

The output of the master process will look something like this:

Factorial of 0 is 1
Fibonacci of 0 is 1
Fibonacci of 1 is 1
Fibonacci of 2 is 2
Fibonacci of 3 is 3
Fibonacci of 4 is 5
Factorial of 1 is 1
Fibonacci of 5 is 8
Fibonacci of 6 is 13
Fibonacci of 7 is 21
Fibonacci of 8 is 34
Fibonacci of 9 is 55
Factorial of 2 is 2
Factorial of 3 is 6
Factorial of 4 is 24
Factorial of 5 is 120
Factorial of 6 is 720
Factorial of 7 is 5040
Factorial of 8 is 40320
Factorial of 9 is 362880

Note that the results come back in an arbitrary order, which is a direct result of the bag data structures our application uses. The set of tasks we generate resides in a bag data structure, from which the workers can pick out tasks in any order. Likewise, the result pool is a bag of entries, from which results can be retrieved and displayed in any order.

We will return to the topic of compute servers in Chapter 11, where we will build a more sophisticated server and use it to solve an interesting, compute-intensive decryption problem.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset