CS 358. Concurrent Object-Oriented Programming
Spring 1996

Lectures 11-13. Obliq

References:
L. Cardelli, A language with distributed scope ACM Symp. Principles of Programming Languages, 1995?, pages ??-??. (see web page below).

Main Concepts

Obliq is a relatively small, interpreted language for distributed computation. Overall, the main innovation in the language seems to be the way that the object model and primitive operations support the distribution of objects (and the data they contain). The concurrency primitives are relatively "standard" fork and join of threads with mutex and locking for concurrency control.

The main concepts in the language are:

Network scoping is an issue in the presence of higher-order distributed computation: Suppose a site accepts procedures and executes them. What happens to free variables in the procedure? Obliq takes the view that such identifiers are bound to their original locations, as prescribed by lexical scoping, even when these identifiers belong to different network sites.

Question: how would you handle higher-order procedures without static scoping?

In comparison with other systems we have looked at, the strengths of Obliq are its treatment of serialization and distribution.

                     Actors               Rapide                    Obliq
                     
Communication     asynchr message send   broadcast             synchr message send

Serialization     serial objects         multi-thread objects  multi-thread objects
                                                                 serialized objects
                                                                 implicit, explicit mutex
                                                               
Distribution      implicit               implicit              explicit 
                                                               
Encapsulation     objects                objects               weak object forms

Object model

Obliq uses objects, but no classes. Object may be produced directly or cloned from other objects. There is no inheritance mechanism. One reason for these design decisions is that each object may be constructed as a self-contained entity that can be replicated across the network.

Objects are collections of named fields, which may be data or methods, with four basic operations

Any object may become accessible over the network either by "mediation of a name server" (whatever that is; maybe cloning??) or by being used as an argument or result of a remote method. Each object is local to a single site, to avoid state duplication, but network references may be transmitted freely.

Methods and Functions: Obliq has separate syntax for methods and procedures.

meth (self)  self.x + y end

proc (x)  x+3 end

Examples:

   let 1d_point = 
      {  x     => 3,
         move  =>meth (self, dist)  self.x := self.x + y; self end
      }

Distribution model

The four main concepts in the distribution of computation are:

Values and locations:
In Obliq, constant identifiers denote values, variable identifiers denote locations. (This is like ML.)

Transmission:
A value may contain embedded locations. For example, an array has a location for each entry, an object a location for updatable fields, and a closure a location for each free variable identifier. On transmission, a value containing no embedded locations is copied. A value containing embedded locations is copied up to the point where locations appear. Then, local references to locations are replaced by network references.

Closures:
In general, a closure consists of source code (or compiled code) and its environment, typically represented by a pointer to an evaluation stack. One way to transmit closures would be to copy an entire evaluation stack. Instead, Obliq represents a closure using a table of values in place of a stack pointer. This table contains only the values of free variables, not the entire stack, and may be transmitted with the source code. However, the table may contain references to remote locations (after transmission) if the code has free variable identifiers.

Objects:
An object consists of a set of locations at a single site. While references to an object may be passed across the network, an object remains at a single site. However, an object may be "cloned" to a different site, duplicating its state in the process. Aliasing provides ... (?? remote acess to an object ??)

Concurrency and mutual exclusion:
The basic mechanism for producing concurrent computation is explicit creation of sequential threads. This is done through explicit fork and join statements. Presumably (* check the manual *) fork creates two new threads that replace the exisiting thread and proceed concurrently. The join statement is used to wait for forked threads to complete (what if they, in turn, fork? Is this like cobegin/coend??) and then continue a single thread. (* this is not explained in the POPL overview paper, but see queue example below *)

Related primitives are:
condition() | signal(a) | broadcast -- create and signal a condition
watch s_1 until s_2 -- waiting for a signal and a boolean guard
pause(a) -- pause the current thread
mutex() -- create a mutex
lock s_1 do s_2 end -- locking a mutex in a scope
wait(a_1,a_2) -- waiting on a mutex for a condition

It looks like the novel aspect of Obliq is the way objects (data) are transmitted to different locations, not the treatment of concurrency (control of execution).

I gather that communication, in the form of sending a message to an object, is synchronous.

More about objects

A simple example

     let p = {  x => 0, y => 0,                     (* define point object *)
                move => meth (self,distx, disty) 
                          self.x = self.x + distx;
                          self.y = self.y + disty;
                          self;
                  end
              };
              
              
      p.x                                            (* select component *)
      p.x := 5;                                      (* set component *)
      p.move(2,1);                                   (* invoke method  *)
      p.move  :=  meth (self,distx, disty)           (* override method *)
                          self.x = self.x - distx;
                          self.y = self.y - disty;
                          self;
                  end

Operations on Objects

Selection

a.x for selection of field, a.x(p1,...,pn) for invoking a method with parameters

Updating

a.x := e updates field x of object a. It is possible for e to be a method if a.x is not, or conversely. For example, we can set point p above so that it always stays on the diagonal as follows:
      p.y     :=  meth (self) self.x  end            (* override method *)

      p.move  :=  meth (self,distx, disty)           (* override method *)
                          self.x = self.x + distx;
                          self;
                  end
This does change the way we select the x component, since we now should write p.y() instead of p.y, I think.

When a field of a remote object is updated, a closure is transmitted over the network and installed in the remote object.

Cloning

Cloning provides a form of multiple inheritance, as well a form object transmission. More specifically,
clone(a_1, ... , a_n)
creates a new object with the same field names as the union of a_1, ... , a_n, each initialized to the coresponding field (values, menthods or aliases) of a_i. Whether the objects named in the clone statement are local or remote, the new object is created at the local site. This may involve transmission of closures across the network.

Cloning provides a form of object extension, for example

clone(p, { color => green, darken => meth(s) s.color.hue := s.color.hue+1 end }
produces a colored point by cloning a point. It's not clear to me whether the added methods are allowed to refer to the methods in the cloned object. I guess that since there are no static type restrictions, this is allowed. (* .. *)

Aliasing

An alias field is defined using the syntactic form
    alias y of b end
If the field x of object a is this alias, then x.a results in invocation (or selection) of field y of object b. An important difference between aliasing and delegation is that if method b of object y uses the self parameter, this will refer to the object b, not the object a that was sent the original message. Example:
    let  b = { w => 0, y => meth(self) self.w end };

    let  a = { w => 1, x => alias y of b end };

    a.y  (* returns 0, not 1 *)
Aliases can be set using object updating, for example, a.w := alias w of b end.

If a.x is an alias for b.y, then method override of a.x results in redefinition of b.y.

A special case of aliasing is redirection of all operations, expressed by

redirect a to b end
The effect is to replace every field of a (including alias fields) to b. If a is local and b remote, then this creates a "local surrogate" for a remote object.

Questions: is aliasing syntactic sugar,

    alias y of b end     for     meth (s) y.b end
or is there something else going on here? What is the effect of
    let b = clone(a);
    redirect a to b end
does this migrate the object?

Protected Objects

An object operation may be invoked either "externally" by code that has a references to the object, or "internally" by one of the methods of the object. For example, in the point example above, the first assignment is an "external" update, while invoking the move method (with body containing self.x := self.x + distx) results in an "internal" update.
      p.x := 5;                          (* external update *)
      p.move(2,1);                       (* move method performs internal updates  *)

In a protected object, written

      {protected ... }
external updating, cloning and aliasing operations are disallowed. However, these may be done internally by methods if desired. This mechanism is useful for maintaining invariants of an object.

The capability to update, clone and alias is transferrable in certain ways. More specifically, we say an operation op(o), where op may be update, clone or alias, is internal or self-inflicted if o is the same object as the self of the current method, if any. The current methods is the last method that was invoked in the current thread of control and has not yet returned. In particular, procedure calls do not hide or change the current method.

Example: An operation internal to a nested object is not considered internal to the outer enclosing object:

let o = { m => meth(s) let o' = {n => meth(s') s.m end } in o'.n end ... }
Note: the internal/external distinction is also important for serialized objects, discussed below.

Question: how do we get encapsulation in Obliq? One approach is through scoping, for example

    let create = proc(init_x)
                     let var x = init_x 
                     in  { get_x => !x,
                           double => meth () x := 2*x end
                          }
Each call to create will allocate a new location for x and return an object closure that has "private" access to this x. However, inheritance via cloning will not result in objects that have their own copy of private x. In other words, this way of achieving "private" instance variables seems reasonable for the objects that are immediately created, but does not work properly (or as usual, anyway) in the presence of inheritance.

Serialized Objects

An Obliq object can be accessed concurrently by multiple threads. One way of preventing race conditions and other problematic interactions between operations is to use sequential objects.

A serialized object, written

      {serialized ... }
has an implicit associated mutex, called the object mutex. An object mutex serializes the execution of selection, update, cloning and aliasing operations according to these specific rules: Note: notion of internal (or self-inflicted, as the Obliq documentation calls this) is not statically determined (it is tested at run-time), and interacts badly with inlining of methods calls (see nested object examples above)...

Explicit signaling and waiting: A condition may be created using the expression condition() and "slgnaled" using the expression signal(a) .

A watch statement makes it possible to coordinate multiple threads in an object, using signals and the implicit mutex of an object. Intuitively, the command

watch c until guard end
waits until the boolean-valued guard becomes true, with the condition c used to determine when to check the guard again if it is currently false. It is generally used when the object mutex is locked, unlocking the mutex if the thread waits because the guard fails. More specifically, this statement evaluates c to a condition and, if the guard evaluates to true, terminates the wait leaving the object mutex locked. If the guard is false, the object mutex is unlocked (allowing other methods of the object to execute) and the thread waits for the condition to be signalled. When the condition is signaled, the object mutex is locked and the boolean guard evaluated again, repeating the process.

Example: a serialized queue, using scoping for encapsulation (taken from Cardelli's slides).

let queue = 
   (let NonEmpty = condition();
    var q = [];                      (* the hidden queue data *)
    
    {protected, serialized,
        write =>
           meth(s, elem)
              q := q @ [elem];       (* append elem to tail          *)
              signal(nonEmpty);      (* wake up readers              *)
           end;   
        read =>
           meth(s)
              watch nonEmpty         (* wait for writers             *)
              until #(q)>0  end;     (* until number of elements > 0 *)
              let q0 = q[0];         (* select first element         *)
              q := q[1 for #(q)-1];  (* remove from queue            *)
              q0;                    (* return first element         *)
           end;
      }
   );
A simple multi-threaded use of this queue
let t = fork( proc() queue.read() end, 0);       (* fork a reader, which blocks *)
queue.write(3);                                  (* write to queue              *)
let result = join(t)                             (* wait for reader thread and return value *)

Exercise:
Suppose we add another method to queues that just reads the front of the queue without deleting it.

        front =>
           meth(s)
              watch nonEmpty         (* wait for writers             *)
              until #(q)>0  end;     (* until number of elements > 0 *)
              let q0 = q[0];         (* select first element         *)
              q0;                    (* return first element         *)
           end;
The semantics of signal are that at least one blocked process will receive the signal and test its guard. With several front operations waiting for a condition, only one would be "woken up" and proceed. Explain this and show that the problem can be fixed by changing "signal" in write to "broadcast".

Object Migration

page from paper

slide 12: name server

slides 18-19: shared variables as a result of static scope (remember: function call is synchronous so rexec completes before subsequent value of x is tested)


Can you test whether an object is local or remote? Would this be a good extension to Obliq?

Fault tolerance: what do you do if an object fails to respond to a message? (Communication appears synchronous...)