[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]

Java Threads (Workshop)



//{{{}}}

Dear All,

One of the other things resolved at WoTUG-19 was for WoTUG to put on a
"Java Threads Workshop" sometime next September (October?).  The reasoning
being that there's not much yet writtem about exploiting concurrency with
Java and that the occam community may have something to offer in thinking
this through.

Anyway, to get going I had a go using the Piped Input/Output Stream classes
that Oyvind Teig gave us an informal talk about during one of the WoTUG-19
evening SIGs.  Oyvind has been using them to get synchronised occam-like
communications between threads.  If this can be nailed down, it would be
a very good thing.

Anyway, I tried using them to set up a single integer channel between two
Java threads and it didn't work ... the received integers were being
mysteriously corrupted sometimes.  I sent my code to Oyvind and he quickly
found the problem.  Although the Piped Input/Output Stream classes had
methods that were definitely labelled as carrying ints, they quietly
truncated them to bytes on the way!

The documentation, however, did tell me only to send bytes through the pipe
... but I hadn't, of course, read that.  I don't really think it's acceptable
to document safety rules for a method that are not enforced automatically
(especially when they could be enforced ... Java does understand about strong
typing).  This is as bad as documenting safety rules for a language and not
enforcing them automatically.  Someone's going to break them and not find out
until it's too late!

Note: Oyvind's CommsTime program would have given the wrong answers ... but
I don't think it managed to count up to numbers larger than a byte ... time
was too short?  (:-)

This problem apart, we need to find out what the true semantics of these
streams are, how they are implemented and how they relate to occam.

Enclosed is my last reply to Oyvind which contains some thoughts on all
these things.  Any comments?

Peter.

------------------------------------------------------------------------------
The following enclosure is folded -- best viewed with Origami or F.
------------------------------------------------------------------------------

//{{{  enclosed

Dear Oyvind,

//{{{  Thanks for solving my problem with Piped Streams in Java

Many thanks for finding the problem I'd run into with Piped Input/Output
Streams in Java.  I find the explanation astonishing -- Java knows about
typing and there's not much of an excuse for Sun to have put out code
like that!

I suppose they could say I should have read the documentation ... but
enforcing semantics by documentation when it could have been done by the
language (i.e. the method interface in this case) is plain silly.  That's
what we have to do in low-level languages like assembler or C.  We should
be doing much better than that with Java.

//{{{  their problem with the read method and what they should have done

The problem they had was wanting to return an end-of-stream code to a
reader of the pipe when the pipe has been closed down.  They chose to
return a -1.  Hence, they had to declare the parameter as an int and
not a byte.  Having done that, they wanted to stop writers sending down
ints, which would cause a (byte) type cast to fail (when writing into
a byte array).  They should let the type cast fail!  That way the user
will realise the misuse of the pipe.  Silently truncating ints down to
bytes in the implementation is horrendous!!  Why don't they throw an
exception.  Better still, declare the parameter to write as a byte!!!
There's no reason for that not to be a byte ... then, the pipe can't be
misused.

//}}}
//{{{  a better way to solve it

Best of all, don't return a -1 to indicate end-of-stream.  The `read'
method needs to return two things: a success indication and the byte
read, if successful.  The obvious thing in Java is to throw an exception
when the stream has ended:

  public abstract byte read() throws IOException, EndOfStream;

where EndOfStream extends one of the standard exception classes.

//}}}
//{{{  an alternative better way to solve it

Alternatively, the `read' method needs to return two things: a success
indication and the byte read, if successful.  If we don't want to throw
an exception and we don't want to code the two items into an int, we
have to get Java to return the two items.  But Java methods only return
one thing and we don't have records.  Also, we don't have explicit
reference parameters for methods ... but arrays and objects are implicitly
passed by reference.  Hence, we could have:

  public abstract void read (ByteOK result) throws EndOfStream;

where:

  public class ByteOK {       // effectively a record
    public byte b;
    public boolean ok;
  }

It's also much better to set up this method as a prodedure than as a
function ... it certainly has a side-effect!

//}}}

//}}}
//{{{  But, what are the semantics of these Piped Streams?

I've tried going through the code for Piped Input/Output Streams and find
it very hard going.  As you said, it seems to be possible for any number
of threads to write and any number to read.  The current implementation
seems to give direct synchronisation between a writer and a reader, but
there's a circular buffer in there (that doesn't look implemented) that
will presumably break this direct synchronisation?

Anyway, is this correct for the current implementation?  Any writer may
write to a PipedOutputStream at any time but will be blocked until a
reader appears.  Any reader may read from a PipedInputStream at any time
but will be blocked until a writer appears.  The data is copied from
the writer to the reader indirectly through the buffer.

The code implementing these things is incredible -- two extra threads
plus a busy-polling loop?

//{{{  how to get the same many-many communication semantics in occam

Well, in occam3 actually.  We could do this in occam2.1 but it really helps
to have SHARED channels ... or, even better, SHARED CALL channels.  Consider:

//{{{  PROC sync.int (SHARED CALL write (VAL INT w), SHARED CALL read (INT r))
PROC sync.int (SHARED CALL write (VAL INT w), SHARED CALL read (INT r))
  WHILE TRUE
    ACCEPT write (VAL INT w)
      ACCEPT read (INT r)
        r := w
:
//}}}

To use it, just declare the two SHARED CALL channels and set up the PAR
construct with an instance of sync.int and all the processes that want to
communicate through it.  With a proper implementation of SHARED CALL
channels, sync.int is just used to synchronise and the data is copied
directly from the workspace of the writer into the workspace of the reader.
Writers queue up automatically until a reader appears.  Readers queue up
automatically until a writer appears.  The implementation of sync.int is
trivial.  I think occam has the better primitives for this kind of thing
than does Java!

//{{{  it's even better with MODULEs

occam3 MODULEs let us wrap up those channel declarations and process
instantiation much more neatly.  Consider:

//{{{  MODULE TYPE SYNC.THING ()
MODULE TYPE SYNC.THING ()
  INTERFACE
    SHARED CALL write (VAL THING w):
    SHARED CALL read (THING r)):
  TO
    SERVER
      //{{{  ACCEPT write (VAL THING w)
      ACCEPT write (VAL THING w)
        ACCEPT read (THING r)
          r := w
      //}}}
        SKIP
:
//}}}

where we have generalised it to work with any data type THING.  The user
now declares the single object (and that's a `real' object with it's own
thread of control):

MODULE sync.thing IS SYNC.THING ():

Now, to find a reader, all a writer does is call:

  sync.thing[write] (my.thing)

To find a writer, all a reader does is call:

  sync.thing[read] (my.thing)

where we can have any number of parallel readers/writers wanting to find
partners with which to synchronise.

//}}}

//}}}

//}}}

Cheers,

Peter.

cc: occam-com@xxxxxxxxx
//}}}