The next example shows the alternative implementation with the commit point in the reply phase. This could be used by the client with a 1 second time-out with code like this.
val time_out: int -> int CML.event ... select [Counter.getEvt cnt, time_out 1] |
As soon as the select starts waiting for these events I want the request to be sent to the counter. If the reply is not ready before the time-out event is enabled then the reply must be discarded. Note that for type correctness the time-out event must be able to deliver an integer the same as the counter does.
The first version of this example uses a channel to carry the reply. When the counter attempts to reply it must not become blocked while waiting for the client to accept the reply, otherwise we will lose concurrency in the program. Since sending to a channel can block, a separate thread must be spawned to deliver the reply while the counter goes on to handle the next request. So we can have more than one outstanding reply to different clients. There will have to be a different reply channel for each client.
Similarly the client should not be blocked waiting for the counter to receive the request as this may delay the start of the time-out or prevent delivery of the time-out event. So the request will be sent from a separate thread as well.
Here are the message types.
structure Counter: COUNTER = struct datatype Request = ReqIsIncr of int | ReqIsGet of Reply CML.chan and Reply = ReplyIsCount of int and Counter = Counter of { req_chan: Request CML.chan } |
The Counter record no longer holds a reply channel. Instead one is passed along with the ReqIsGet message. Here is the new function with the updated counter implementation. I've added a time delay into the reply for testing. If you change the delay to 2 seconds the client, shown below, will time-out.
fun new init = let val req_chan = CML.channel() fun counter() = let fun loop count = ( case CML.recv req_chan of ReqIsIncr n => loop (count + n) | ReqIsGet rpl_chan => let fun reply() = ( delay 0; CML.send(rpl_chan, ReplyIsCount count) ) in CML.spawn reply; loop count end ) in loop init end val thread = CML.spawn counter in Counter { req_chan = req_chan } end |
Here is the time delay function. Don't use the Posix.Process.sleep function or anything similar as it will pause the entire program, not just one thread. See the section called More on Time-Outs for more details.
fun delay n = CML.sync (CML.timeOutEvt (Time.fromSeconds n)) |
Here is the updated implementation of the getEvt function.
fun getEvt (Counter {req_chan, ...}) = let fun send() = let val rpl_chan = CML.channel() fun recv (ReplyIsCount n) = n fun sender() = CML.send(req_chan, ReqIsGet rpl_chan) in CML.spawn sender; CML.wrap(CML.recvEvt rpl_chan, recv) end in CML.guard send end |
This returns an event that is constructed by the recvEvt function and is wrapped in both the send and recv functions. The guard delays the sending of the message until an attempt is made to synchronise on the event using for example select as shown above. Then a thread is spawned to send the Get request asynchronously and an event to represent the reception of the reply is constructed. This event is wrapped with the recv function to unpack the integer count in the reply. It is this receive event that is returned and waited on by the client along-side the time-out.
Finally to demonstrate the time-out I've changed the driver function.
fun run() = let val obj = Counter.new 0 fun time_out t = CML.wrap( CML.timeOutEvt(Time.fromSeconds t), fn () => ~1) in Counter.incr obj 3; Counter.incr obj ~1; let val c = CML.select [Counter.getEvt obj, time_out 1] in print(concat["The counter's value is ", Int.toString c, "\n"]) end end |
Here I've added a time_out function that delivers a count of -1 if the time-out expires.
It might seem that there is a risk that one of the spawned threads will get stuck forever if either the client or the counter fails to complete the interaction. But the garbage collector can determine if a channel operation can never complete because no other threads reference the channel. So all of these spawned threads will be cleaned up properly even if there is a time-out.
Figure 6-6 shows the client getting the counter's value with and without a time-out. When there is no time-out, the select will choose to receive the reply. When there is a time-out the select will receive a time-out message that is triggered by the CML.timeOutEvt function. The reply thread running the reply function will block indefinitely. When the garbage collector collects the event from getEvt all references to the reply channel outside of the reply thread will disappear and the reply thread will be collected too.
Here is a more stream-lined version using an I-variable (see the section called Synchronous Variables) to return the reply. Writing into an I-variable never blocks. If there is already a value in the variable then that is an error which raises an exception. But the counter will only reply once. So the counter doesn't need to protect itself against blocking by spawning a thread for the reply.
structure Counter: COUNTER = struct datatype Request = ReqIsIncr of int | ReqIsGet of int SyncVar.ivar and Counter = Counter of { req_chan: Request CML.chan } ... omitted material ... |
fun counter() = let fun loop count = ( case CML.recv req_chan of ReqIsIncr n => loop (count + n) | ReqIsGet rpl_var => ( (* delay 2; *) SyncVar.iPut(rpl_var, count); loop count ) ) in loop init end ... omitted material ... |
fun getEvt (Counter {req_chan, ...}) = let fun send() = let val rpl_var = SyncVar.iVar() fun sender() = CML.send(req_chan, ReqIsGet rpl_var) in CML.spawn sender; SyncVar.iGetEvt rpl_var end in CML.guard send end |