[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]
RE: Channels versus Methods
This is a spell-checked, split-infinitive-repaired, typo-removed and slightly
corrected version of my posting from yesterday. Also, note that the "Last Last
Last" point has been deleted ;-)
HOW TO CONVERT A METHOD INTERFACE INTO A (SHARED VARIANT) CALL CHANNEL
======================================================================
Base Plan
=========
Consider an object, B, with a method interface, Foo, and an object, A, that
wishes to `communicate' with B using Foo. For example:
///////////////////////////////////////////////////////////////////////
interface Foo {
public Thing calculate (...);
public void processQuery (...);
public Silly closeValve (...);
}
///////////////////////////////////////////////////////////////////////
All we do is give A a constructor (or mutator method) that takes a Foo
argument, construct an instance of A and give it an instance of B (where
B implements Foo). This is standard Java OO ... :-(
Now, suppose we want A and B to be active processes with their own thread(s)
of control. Sticking with the above is an open invitation to race hazards
and countering them leads us into all that Java monitor goo. Instead, we
need to convert that Foo interface into a (SHARED) variant CALL channel.
[Aside: occam3 did not propose *variant* CALL channels - I'll mail about
this separately.]
A simple low-fuss way of doing this is to connect A and B by an ordinary
channel:
______________________ ______________________
| | | |
| | c | |
| A |------->-------| B implements Foo |
| | | |
|______________________| |______________________|
The channel is not used to communicate any data. It's used purely for
synchronising the calls to the Foo interface of B by A - channels are
really simple things to use for synchronisation!
So, if A wishes to call any of the Foo methods, it first must obtain
permission from B - who will then block until told the call is complete:
c.write (null); // ready to make the CALL
Thing result = B.calculate (...); // or some other Foo method
c.write (null); // the CALL is complete
At the other end, B must choose to service the CALL and then wait until
it's all over:
c.read (); // ready to ACCEPT the CALL
c.read (); // wait until the CALL is complete
where, of course, the first channel input can be part of an ALT :-).
We have the full flexibility of method invocation - an arbitrary number of
parameters and an optional result. It's variant since *any* of the Foo
methods may be invoked by the CALL.
We have full occam/CSP synchronisation - the CALL cannot proceed until both
processes agree to participate in it. Whoever commits first has to wait for
its partner. Crucially, either side may refuse to play ball.
The actual Foo method call is made by the caller process. But the callee
process is suspended. Semantically, it is the same as the caller and callee
processes being `joined' for the duration of the call. This is the semantics
we want.
To make a SHARED CALL channel (i.e. many callers), we can't simply use
a many-1 channel for c (because the channel needs to be locked to a single
caller for two writes). We could achieve this with a channel technique
like Gerald's claim/release methods. Another way is to use two channels:
the first one is made many-1 and is used for the first communication (the
`claim') and second need only be 1-1 and is used for the second (`release').
Or we can simply lock the CALL sequence using a special lock object (and
leave the channel as 1-1):
Object cLock = new Object ();
...
synchronise (cLock) {
c.write (null); // ready to make the CALL
Thing result = B.calculate (...); // or some other Foo method
c.write (null); // the CALL is complete
}
There are two obvious problems with this simple approach:
(a) A still sees B directly (or, at least, has a reference to it as
a Foo-implementing object - which amounts to the same thing);
(b) both A and B have to abide by the above pattern for the CALL. It
would go wrong if A were just to invoke B.calculate directly ...
or *any* of the channel operations were omitted on either side.
The (a) problem is serious and independent from the (b) one. One of the
beauties of channel interfaces is that they completely decouple processes
from each other. Processes see only their channels. They don't see each
other.
These things apart, the above is a simple enough design pattern that can
be used straight away (with either JCSP or CJT) to give us variant CALL
channels. What follows is an attempt to capture this securely in the JCSP
context.
Securing the Base Plan
======================
We need to wrap up the above design pattern into a class and methods that
enforce it. This class will be the base One2OneCallChannel, from which
users may specialise to produce interface-specific CALL channels ... like
One2OneFooChannel. The full range of Many2One/One2Many/Many2Many CALL
channels will also be made available.
I tried very hard to make a One2OneCallChannel by *inheriting* from the
basic One2OneChannel. That would have given us ALTing for free. As usual,
inheritance doesn't quite work - defeated again by the lack of any safe
multiple inheritance and the fact that Java interfaces may only contain
public (and not package-visible) methods. Sigh. This makes no difference
to the users, but makes it slightly more tedious to do the wrapping.
The Callee-Interface to the CALL Channel
----------------------------------------
We define:
///////////////////////////////////////////////////////////////////////
package jcsp.lang;
public interface ChannelAccept {
public void accept (Object o);
}
///////////////////////////////////////////////////////////////////////
The callee process will have a ChannelAccept field, c say, for the CALL
channel associated with the Foo interface. If it wishes to accept such
at call, all it does is:
c.accept (this);
which registers itself (and, by implication, its willingness to accept
the call) inside the channel. When this method returns, the call (to one
of its Foo methods) will have been made.
If we wish to be able to ALT over CALL channel acceptances, we use:
///////////////////////////////////////////////////////////////////////
package jcsp.lang;
public abstract class AltingChannelAccept extends Guard
implements ChannelAccept {
// nothing to add ...
}
///////////////////////////////////////////////////////////////////////
The reason this isn't an interface is that Guard contains package-visible
method headers (enable/disable) - these are of no concern to JCSP users.
The callee process can add an AltingChannelAccept variable, c say, to a Guard
array used in constructing an Alternative. If its index is selected, we are
committed to respond with:
c.accept (this);
That's it for the callee interface. We can manage any number of interfaces
with individual CALL channels and ALT between them (and ordinary channels,
timeouts and SKIPs).
The Caller-Interface to the CALL Channel
----------------------------------------
This has to be Foo-specific ... in fact, it is just Foo! The caller process
will have a Foo field, c say, into which will be plugged the CALL channel -
not the target process. To make a call, all we do is:
Thing result = c.calculate (...);
or whatever Foo method we wish to CALL.
The Base CALL Channel Classes
-----------------------------
These are concrete implementations of the AltingChannelAccept abstraction --
similar to the way One2OneChannel (etc.) are implementations of AltingChannel.
The difference is that we don't implement the caller side of the CALL channel.
The caller side is application specific and needs to be set up by the user.
Here is the `One2One' base class. It *contains* a One2OneChannel rather
than inherits from it (see above for reasons). This means propagating
its package-visible enable/disable methods (that are used for ALTing)
in the usual trivial way. JCSP users don't need to know or care about
this though.
///////////////////////////////////////////////////////////////////////
package jcsp.lang;
public abstract class One2OneCallChannel extends AltingChannelAccept {
final private One2OneChannel c = new One2OneChannel ();
protected Object target; // made available to the caller
public void accept (Object target) { // invoked by the callee
this.target = target;
c.read (); // ready to ACCEPT the CALL
c.read (); // wait until the CALL is complete
}
protected void sync () { // indirectly invoked by the caller
c.write (null);
}
boolean enable (Alternative alt) { // ignore this!
return c.enable (alt);
}
boolean disable () { // ignore this!
return c.disable ();
}
}
///////////////////////////////////////////////////////////////////////
Ignoring the enable/disable methods, see how the accept works. It's invoked
by the callee process with itself as the parameter. We save that parameter
and commit to reading from the channel. The callee will be blocked here until
the caller makes a CALL (see below).
When the caller makes that CALL, it will write to the protected channel (by
invoking `sync'). The caller will be blocked until the callee has invoked
an accept (see above).
When both thus rendezvous, the callee immediately blocks on reading from
that channel again. Meanwhile, the caller makes its required Foo method
call on the callee target - a reference to which is safely saved in this
CALL channel. When that completes, the caller again writes to the real
channel (c), unblocking the callee. Both, then, go their separate ways.
Application-Specific CALL Channels
----------------------------------
We have set up an inheritance mechanism for letting the user define specific
CALL channels that support the particular method interface need for their
application. Usually, something goes very wrong whenever I try to design
anything for use via inheritance ... so I'm keeping my fingers crossed about
this one. [It could be changed to remove this inheritance mandate if that
proves necessary (and it probably will).]
Anyway, here goes for our Foo interface. Define:
///////////////////////////////////////////////////////////////////////
class One2OneFooChannel extends One2OneCallChannel implements Foo {
public Thing calculate (...) {
sync (); // ready to make the CALL
Thing t = ((Foo) target).calculate (...);
sync (); // call finished
return t;
}
... similarly for processQuery and closeValve
}
///////////////////////////////////////////////////////////////////////
That's all the user has to generate. All that has to be done is surround
the method call with a pair of `sync's and return the result (if any).
All `sync' does is write to the private channel inside One2OneCallChannel.
The explanation in the section above talks through how these work.
Example Use of a CALL Channel
-----------------------------
Let's go back to the earlier example:
______________________ ______________________
| | | |
| | c | |
| A |------->-------| B implements Foo |
| | | |
|______________________| |______________________|
where, this time, c is going to be a CALL channel.
First, here is an outline of an A process:
///////////////////////////////////////////////////////////////////////
class A implements CSProcess {
private final Foo out;
public A (final Foo out) {
this.out = out;
}
... other fields, methods etc.
public run () {
...
Thing t = out.calculate (...);
...
Silly s = out.closeValve (...);
...
out.processQuery (...);
...
}
}
///////////////////////////////////////////////////////////////////////
And here is an outline for a B process:
///////////////////////////////////////////////////////////////////////
class B implements CSProcess, Foo {
private final ChannelAccept in;
public B (final ChannelAccept in) {
this.in = in;
}
... other fields, methods etc.
... implementation of Foo methods (calculate, processQuery and closeValve)
public run () {
...
in.accept (this);
...
in.accept (this);
...
in.accept (this);
...
}
}
///////////////////////////////////////////////////////////////////////
When B commits to an accept, it doesn't know which of its Foo methods will
be called. This is just like a variant (CASE) protocol in occam. If it
isn't prepared to accept certain of those methods, it must only be at times
in the exchange with the caller when it knows the caller won't be making
them. Otherwise, the methods must not be grouped in the same CALL channel
and we must ALT between the resulting different CALL channels, refusing
acceptance on some of them in the usual ways.
It might be an idea to get the accept method to return some indication of
which CALL method was actually invoked by the caller. That's pretty easy
to do - for example, by an extra protected INT field of the base CALL channel
class (One2OneCallChannel) that gets set up by the caller in the user-defined
subclass (e.g. One2OneFooChannel) and returned by the accept method.
Of course, calls and accepts can occur anywhere in the process codes - not
just straight-line stuff as in the above outlines.
Finally, the network code is very neat:
One2OneFooChannel c = new One2OneFooChannel ();
new Parallel (
new CSProcess[] {
new A (c),
new B (c)
}
).run ();
and looks just how it should look.
Many-1, 1-Many and Many-Many CALL Channels
------------------------------------------
As said in the Base Plan, we can't simply replace the One2OneChannel in
the base class, One2OneCallChannel, with a One2ManyChannel (say) and obtain
a safe One2ManyCallChannel (say). Here is one way to do it:
///////////////////////////////////////////////////////////////////////
package jcsp.lang;
public abstract class One2ManyCallChannel implements ChannelAccept {
final private One2OneChannel c = new One2OneChannel ();
final private Object acceptLock = new Object ();
protected Object target; // made available to the caller
public void accept (Object target) { // invoked by the callee
synchronized (acceptLock) {
this.target = target;
c.read (); // ready to ACCEPT the CALL
c.read (); // wait until the CALL is complete
}
}
protected void sync () { // indirectly invoked by the caller
c.write (null);
}
}
///////////////////////////////////////////////////////////////////////
Notice that, as usual, we don't allow ALTing over '2Many channels. To make
the respective One2ManyFooChannel, extend the above class as before:
///////////////////////////////////////////////////////////////////////
class One2ManyFooChannel extends One2ManyCallChannel implements Foo {
public Thing calculate (...) {
sync (); // ready to make the CALL
Thing t = ((Foo) target).calculate (...);
sync (); // call finished
return t;
}
... similarly for processQuery and closeValve
}
///////////////////////////////////////////////////////////////////////
For the Many2' classes, we don't need to provide base classes. Users
specialising the One2OneCallChannel or One2ManyCallChannel base classes
just need to remember on more thing - synchronise the specialist methods.
For example:
///////////////////////////////////////////////////////////////////////
class Many2OneFooChannel extends One2OneCallChannel implements Foo {
public synchronized Thing calculate (...) {
sync (); // ready to make the CALL
Thing t = ((Foo) target).calculate (...);
sync (); // call finished
return t;
}
... similarly for processQuery and closeValve
}
///////////////////////////////////////////////////////////////////////
///////////////////////////////////////////////////////////////////////
class Many2ManyFooChannel extends One2ManyCallChannel implements Foo {
public synchronized Thing calculate (...) {
sync (); // ready to make the CALL
Thing t = ((Foo) target).calculate (...);
sync (); // call finished
return t;
}
... similarly for processQuery and closeValve
}
///////////////////////////////////////////////////////////////////////
Note that callers and callees in the Many2Many case will be (Java)
synchronising on different objects, so they will not be deadlocking
each other!
One Last Point
--------------
I don't like the callee class (like B) advertising a standard method interface
(like Foo). In the above example, there remains the danger that someone might
try and invoke one of the Foo methods directly on an instance of B. Or plug
an instance of B directly into an instance of A! Neither of these would be
a good idea!!!
It's also semantically misleading. B's interface is in its CALL channel --
not its Foo interface! So, at the risk of upsetting the OO community once
again:
> OO-people like method interfaces. If we don't provide a method interface
> to your object, we get the chant: "that's not very OO therefore it's bad".
let's hide that method interface and leave only the channel one visible.
The way to do this is through *process* layering (which imposes absolutely
no run-time penalty):
///////////////////////////////////////////////////////////////////////
class B2 implements CSProcess { // no Foo interface
final private B b;
public B2 (final FooChannel in) {
b = new B (in);
}
public run () {
b.run ();
}
}
///////////////////////////////////////////////////////////////////////
Now try and invoke a Foo method on an instance of B2 (or plug one into A)!
FooChannel, by the way, is just the union of Foo and ChannelAccept:
///////////////////////////////////////////////////////////////////////
interface FooChannel extends Foo, ChannelAccept {}
///////////////////////////////////////////////////////////////////////
This also forces a network builder to use the correct kind of wire when
connecting up to B2. For this trick to work, the various concrete
xxx2xxxFooChannels had better implement FooChannel (rather than just Foo).
The network diagram looks cleaner:
______________________ ______________________
| | | |
| | c | |
| A |------->--------| B2 |
| | <FooChannel> | |
|______________________| |______________________|
and the network code can't be wrong - or it won't compile:
One2OneFooChannel c = new One2OneFooChannel ();
new Parallel (
new CSProcess[] {
new A (c),
new B2 (c)
}
).run ();
Note that any of the xxx2xxxFooChannels could be used to make connections
to A or B2. For the above two process example, only the One2One version
is needed.
One Last Last Point
-------------------
This mechanism isn't quite as good as the (proposed) occam3 one - except
that it exists! In occam3, the accepts operate on local process state.
They do here - but only for class variables ... not on locals declared
within the method executing the accept :-( ...
Peter.