back to API     back to index     prev     next  

Asynchronous calls and futures

Creation of a Future Object

Whenever possible a method call on an active object is reified as an asynchronous request. If not possible the call is synchronous and blocks until the reply is received. In case the request is asynchronous, it immediately returns a future object.

This object acts as a placeholder for the result of the not-yet-performed method invocation. As a consequence, the calling thread can go on with executing its code, as long as it doesn't need to invoke methods on the returned object, in which case the calling thread is automatically blocked if the result of the method invocation is not yet available. Below are shown the different cases that can lead to an asynchronous call.

- No Yes
- No No
Yes No No
No Yes Yes

As we can see, the creation of a future depends not only on the caller type, but also on the return object type. Creating a future is only possible if the object is reifiable. Note although having a quite similar structure as an active object, a future object is not active. It only has a Stub and a Proxy as shown in figure below :



A future object

During its lifetime, an active object can create many future objects. There are all automatically kept in a FuturePool.

Each time a future is created, it is inserted in the future pool of the corresponding active object. When the result becomes available, the future object is removed from the pool. Although most of the methods of the FuturePool are for internal use only and are directly called by the proactive library we provide a method to wait until a result becomes available. Instead of blocking until a specific future is available, the call to waitForReply() blocks until any of the current futures become available. An application can be found in the FutureList class.

HashCode and equals

Any call to a future object is reified in order to be blocked if the future is not yet available and later executed on the result object. However, two methods don't follow this scheme: equals and hashCode. They are often called by other methods from the Java library, like HashTable.add() and so are most of the time out of control from the user. This can lead very easily to deadlocks if they are called on a not yet available object.

hashCode()

Instead of returning the hashcode of the object, it returns the hashcode of its proxy. Since there is only one proxy per future object, there is a unique equivalence between them.

equals()

The default implementation of equals() in the Object class is to compare the references of two objects. In ProActive it is redefined to compare the hashcode of two proxies. As a consequence it is only possible to compare two future object, and not a future object with a normal object.

There are some drawbacks with this technique, the main one being the impossibility to have a user override the default HashTable and equals() methods.

toString()

The toString() method is most of the time called with System.out.println() to turn an object into a printable string. In the current implementation, a call to this method will block on a future object like any other call, thus, one has to be careful when using it. As an example, trying to print a future object for debugging purpose will most of the time lead to a deadlock. Instead of displaying the corresponding string of a future object, you might consider displaying its hashCode.

Asynchronous calls in details

The setup

First, let's introduce the example we'll use throughout this section. Let us say that some piece of code in an instance of class A calls method foo on an active instance of class B. This call is asynchronous and returns a future object of class V. Then, possibly after having executed some other code, the same thread that issued the call calls method bar on the future object returned by the call to foo.

What would have happened in a sequential world

In a sequential, single-threaded version of the same application, the thread would have executed the code of the calling method in class A up to the call of foo, then the code of foo in class B, then back to the code of the calling method in class A up to the call to bar, then the code of bar in class V, and finally back to the code of the calling method in class A until its end. The sequence diagram below summarizes this execution. You can notice how the single thread successively executes code of different methods in different classes.



Sequence Diagram - single-threaded version of the program

Visualizing the graph of objects

Let us first get an idea of what the graph of objects at execution (the objects with their references to each other) looks like at three different moments of the execution:

Sequence Diagram

Let us now concentrate on how and when and by which thread the different methods are called. We have two threads: the thread that belongs to the subsystem A is part of (let's call it the first thread), and the thread that belongs to the subsystem B is part of (the second thread).

The first thread invokes foo on an instance of Stub_B, which builds a MethodCall object and passes it to the BodyProxy as a parameter of the call to reify. The proxy then checks the return type of the call (in this case V) and generates a future object of type V for representing the result of the method invocation. The future object is actually composed of a Stub_V and a FutureProxy. A reference onto this future object is set in the MethodCall object, which will prove useful once the call is executed. Now that the MethodCall object is ready, it is passed as a Request to the Body of the Active Object as a parameter. The body simply appends this request to the queue of pending requests and returns immediately. The call to foo that an A issued now returns a future object of type Stub_V, that is a subclass of V.

At some point, possibly after having served some other requests, the second thread (the active thread) picks up the request issued by the first thread some time ago. It then executes the embedded call by calling foo on the instance of B with the actual parameters stored in the MethodCall object. As specified in its signature, this call returns an object of type V. The second thread is then responsible for setting this object in the future object (which is the reason why MethodCall objects hold a reference on the future object created by the FutureProxy). The execution of the call is now over, and the second thread can select another request to serve in the queue and execute it.

In the meantime, the first thread has continued executing the code of the calling method in class A. At some point, it calls bar on the object of type Stub_V that was returned by the call to foo. This call is reified thanks to the Stub_V and processed by the FutureProxy. If the object the future represents is available (the second thread has already set it in the future object, which is described in figure below, the call is executed on it and returns a value to the calling code in A.



Sequence Diagram

If it is not yet available, the first thread is suspended in FutureProxy until the second thread sets the result in the future object (see figure below).



Sequence Diagram


Copyright © April 2004 INRIA All Rights Reserved.