More about tasks and capabilities
In the example above, the variable counter holds a capability. What is a J-Kernel capability, and what actually happens in a call to counter.incrementCount()? Roughly, a capability is a stub object that manages cross-task communication. The stub's class is generated at run-time. If you print the name of this class:
System.out.println(counter.getClass());
you'll get the name of the generated class used for the capability:
class CounterImpl$STUB$LRMI
This class extends the class Capability, and also implements the remote interfaces of CounterImpl (of which there is only one, Counter).
In the call to counter.incrementCount(), the counter capability does the following:
Seeding a new task is one way of creating a capability. A task can also create its own capabilities by calling the method Capability.create. This method takes a target object and produces a capability that implements all the target's remote interfaces. For example:
Counter target = new CounterImpl(); Capability cap = Capability.create(target);
Once a capability is created, it can be passed around from task to task through cross-task calls. Unlike ordinary objects, capabilities are passed straight through cross-task calls without getting copied. In this way, capabilities are the only kinds of objects that can be shared between tasks.
Objects that aren't capabilities can only be passed by copy through cross-task calls. In order for an object to be passed by copy, its class must implement one of the following interfaces:
The Serializable interface is a built into Java. It allows objects to be converted to byte streams and then reconstituted back into objects (this is great for storing objects on disk or sending objects over the network). The J-Kernel can use serialization to make a copy of an object: first it serializes the object into a byte stream, and then it deserializes the byte stream to create a new object which is a copy of the original object. The advantage of using serialization to copy objects is that many standard Java classes, like java.util.Vector, already implement Serializable. The disadvantage is that serialization and deserialization are rather slow. Therefore when you write your own classes, it is better use the J-Kernel's fast copy mechanisms, which copy objects without going through an intermediate byte stream.
When an object is copied, a deep copy of the object is made. This means that first the object is copied, then the objects held in the object's fields are copied, then the objects held in these object's fields are called, an so on. In other words, the copy routines recursively walk through all the objects reachable by the original object, copying each one of them in turn. However, suppose that an object is reachable through more than one path. For instance, suppose object A points to objects B and C, and objects B and C both point to object D. If these objects implement FastCopyTree, then A will be copied as follows: first A is copied into a new object (call this A'). A points to B, so recursively B is copied to form B', and B points to D, so D is copied to form D'. A also points to C, so C is copied to form C', and C points to D, so D is copied again to form D''. So the copy A' points to B' and C', B' points to D', and C' points to D''. Notice how two copies of D were made, because D was reachable through two different paths. Sometimes this is undesirable. For instance, if there is a cycle in the path, then FastCopyTree's copy routine would try to make an infinite number of copies of some objects. To prevent this, FastCopyGraph keeps a hash table of all the objects that were already copied, and consults this table to make sure each object is copied only once. An object implementing FastCopyGraph will only get copied once during a cross-task call, no matter how paths it is reachable from. The names "tree" and "graph" are used because FastCopyTree correctly copies a tree-shaped data structure, while FastCopyGraph correctly copies any data structure (i.e. the data structure's shape can be an arbitrary graph).
If two tasks exchange fast copy objects of class A, then the two tasks must share the class A, in order to ensure that both tasks agree on the format of the class. If each class had its own version of A, then the fast copy routines wouldn't know how to convert one version to the other version. By contrast, serializable classes need not be shared, because Java serialization is able to deal with differing versions of classes to some extent.
Let's look at an example.
In addition to to Counter classes above, will add another class that is designed to be passed by copy:
// IntHolder.java import cornell.slk.jkernel.core.FastCopyTree; public class IntHolder implements FastCopyTree { private int i = 0; public int incrementCount() {return ++i;} }
Now we'll make a capability which takes as arguments a Counter capability (passed by reference) and an IntHolder object (passed by copy):
// ArgTest.java import cornell.slk.jkernel.core.Remote; import cornell.slk.jkernel.core.RemoteException; public interface ArgTest extends Remote { void invoke(Counter counter, IntHolder intHolder) throws RemoteException; } // ArgTestImpl.java import cornell.slk.jkernel.core.Remote; import cornell.slk.jkernel.core.RemoteException; public class ArgTestImpl implements ArgTest { public void invoke(Counter counter, IntHolder intHolder) throws RemoteException { for(int i = 0; i < 10; i++) { counter.incrementCount(); intHolder.incrementCount(); } } }
The following test program creates a child task, seeds the task with an ArgTestImpl object, and invokes this seed object to modify a Counter and an IntHolder:
// Hello.java import cornell.slk.jkernel.core.Capability; import cornell.slk.jkernel.core.Task; import cornell.slk.jkernel.core.SharedClass; import cornell.slk.jkernel.core.Resolver; import cornell.slk.jkernel.std.Start; import cornell.slk.jkernel.util.TableResolver; import cornell.slk.jkernel.util.CompoundResolver; public class Hello { public static void main(String[] args) throws Exception { System.out.println("Hello."); Counter target = new CounterImpl(); Capability cap = Capability.create(target); // Share the classes Counter, IntHolder, and ArgTest: TableResolver sharedResolver = new TableResolver() .setMapping("Counter", Task.shareClass("Counter")) .setMapping("IntHolder", Task.shareClass("IntHolder")) .setMapping("ArgTest", Task.shareClass("ArgTest")) ; // Create a resolver for the new task: Resolver childResolver = new CompoundResolver( new Resolver[] { Start.data.standardResolver, sharedResolver, Start.data.mainResolver }); // Create the child task: Task child = new Task(childResolver); // Seed the child task with an ArgTestImpl object, // and get the capability for this object: ArgTest argTest = (ArgTest) child.seed("ArgTestImpl"); // Create a Counter capability: CounterImpl counterImpl = new CounterImpl(); Counter counter = (Counter) Capability.create(counterImpl); // Create an IntHolder object: IntHolder intHolder = new IntHolder(); // Make a cross-task call into argTest: argTest.invoke(counter, intHolder); // See what effect argTest had on counter: System.out.println("counter:"); System.out.println(counter.incrementCount()); System.out.println(counter.incrementCount()); System.out.println(counter.incrementCount()); // See what effect argTest had on intHolder: System.out.println("intHolder:"); System.out.println(intHolder.incrementCount()); System.out.println(intHolder.incrementCount()); System.out.println(intHolder.incrementCount()); } }
The output is:
Hello. counter: 11 12 13 intHolder: 1 2 3
This shows that argTest modified the counter object (which makes sense, because this was passed by reference). However, argTest only modified a copy of the intHolder object, so that the original was unaffected.
A capability can be revoked at any time by the task that created the capability, by calling the revoke method on the capability. When a capability is revoked, its pointer to the target object is set to null. This prevents anyone from using the capability to affect the target object, and makes the target object eligible for garbage collection, if there are no other pointers to it.
Finally, an entire task can be stopped by its parent task, if the parent calls the terminate method on the child task. When a task is terminated, all its capabilities are revoked, all its thread segments are stopped, and all its children are terminated as well.