Getting the Counter's Value

The next goal is to be able to return the count from a counter object. Since the object can only respond to one message at a time it will be enough to have one channel reserved for replies. The main design goal is ensuring that the channels are used correctly so that the clients of the object can't cause deadlocks.

Remember that channels are synchronous. The client will block while waiting for the counter to receive a get request. When the counter has received it it will send the reply on the reply channel and the client will be blocked waiting for the reply. For correctness there must be a 1 to 1 match between request and reply and the thread that accepts the reply must be the one that sent the request.

To ensure correctness the bits and pieces of the object must be hidden within a module. Here is the signature of a structure that describes the interface to a counter object.

signature COUNTER =
sig
    type Counter

    val new:    int -> Counter 
    val incr:   Counter -> int -> unit
    val get:    Counter -> int

end

To a client, the counter object is represented by a proxy of type Counter. The proxy encapsulates the sending and receiving performed by a client. New objects are created with the new function which takes an initial value for the count. The incr and get functions operate on the counter.

Here are the types that implement the counter.

structure Counter: COUNTER =
struct

    datatype Request = 
            ReqIsIncr of int
        |   ReqIsGet

    and Reply =
            ReplyIsCount of int

    and Counter = Counter of {
                    req_chan:   Request CML.chan,
                    rpl_chan:   Reply CML.chan
                    }

The Request and Reply types define the messages passed between the client and counter. The counter proxy is a record containing the two channels that communicate with the counter.

There is no need to retain any other handle to the counter, such as its thread. As long as there are channels that can communicate with a thread it will be considered to be live by the garbage collector. If a value of type Counter becomes garbage then the thread that it communicates with will also become garbage and the thread will be collected and terminated. (If I want a thread to stay alive without channels then I would have to retain its thread ID somewhere).

Here is the implementation of the counter in the new function.

    fun new init =
    let
        val req_chan = CML.channel()
        val rpl_chan = CML.channel()

        fun counter() =
        let
            fun loop count =
            (
                case CML.recv req_chan of
                  ReqIsIncr n => loop (count + n)

                | ReqIsGet => 
                (
                    CML.send(rpl_chan, ReplyIsCount count);
                    loop count
                )
            )
        in
            loop init
        end

        val thread = CML.spawn counter
    in
        Counter
        {
            req_chan = req_chan,
            rpl_chan = rpl_chan
        }
    end

To create a counter I create the two channels and spawn a thread to run the counter function. The counter function gets its channels and initial value from its surrounding scope. I return a value of type Counter to the client.

Here are the interface functions.

    fun incr (Counter {req_chan, ...}) n =
    (
        CML.send(req_chan, ReqIsIncr n)
    )


    fun get  (Counter {req_chan, rpl_chan}) =
    (
        CML.send(req_chan, ReqIsGet);

        case CML.recv rpl_chan of
          ReplyIsCount n => n
    )

The incr function just sends a message to the server to increment its value. There is no need for a reply. The get function stops to wait for a reply.

Figure 6-5 shows an interaction between the client and object for the get function. The counter has performed a recv and is blocked waiting for the client. When the client sends the Get message the counter runs and sends back the reply. Then it loops and blocks on a recv again. Meanwhile the get function has blocked waiting for the reply. When it gets it it returns the value to the caller.

Figure 6-5. Getting the Counter's Value

The get function is guaranteed to be atomic because of the synchronous nature of the send operation. If another thread attempts to call incr or get before a previous reply has been received then it will block at the send operation. The object will not receive the next message until the reply has been accepted by the client.