|
![]() |
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.
Return type | Can throw checked exception | Creation of a future | Asynchronous |
---|---|---|---|
void | - | No | Yes |
Non Reifiable Object | - | No | No |
Reifiable Object | Yes | No | No |
Reifiable Object | 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 :
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.
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.
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.
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.
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
.
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.
foo
, we have exactly the same setup as
after the creation of the active instance of B
and summarized
in the figure below : an
instance of class A
and an active instance of class B
. As
all active objects, the instance of class B
is composed of a
stub (an instance of class Stub_B
, which actually inherits
directly from B
), a BodyProxy
, a Body
and the
actual instance of B
.
foo
has returned, A
now holds a reference onto a future object representing the
not-yet-available result of the call. It is actually composed of a
Stub_V
and a FutureProxy
as shown on the figure below.
foo
on the instance of B
,
the thread of the Body
sets the result in the future, which
results in the FutureProxy
having a reference onto a V
(see figure below).
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
.
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).