Tuesday, July 17, 2012

[O]Flux: En garde!

Tasty.


Even event-based programs tend to have some state.  Its best if that state is minimal, and shared as narrowly as possible, accessed as briefly as possible and viewed with suspicion when evaluating performance bottlenecks.  Message passing systems like Akka or Erlang tend to avoid this problem my locating state within one thread which runs the Actor which owns that state.  That has some advantages for that model, but it lacks some flexibility when we consider that machines have lots of memory these days and that memory is meant to be shared (safely of course).

The way this problem was solved in Flux was with atomic constraints.  In OFlux these were generalized to guards (at first there were only exclusive guards).  Suppose we had a object foo of type Foo which we wanted to allow multiple node functions to access -- but only one event at a time.  We would declare an exclusive guard to do this job in our OFlux code as follows:


  exclusive aFooGuard () => Foo *;

A node Bar would would be granted access to the Foo * & underlying this using a declaration like:

  node Bar (InputType1 anInput1,..., guard aFooGuard()) 
     => (OutputType1 anOutput1,...);

At the C++ level our implementation of Bar will have an argument atoms which can be used to get our Foo * & with atoms->aFooGuard().  A node event which has a guard in its declaration like this is said to acquire the guard before entry and it releases it after the node event has executed.  Initially this will be a reference to a NULL pointer, but we can change that since it is a reference (allocate a new Foo).  Without the "right hand value" part to store a Foo *, you have basically what Flux used for atomic constraints.

Furthermore, OFlux extends atomic constraints by allowing some data to be stored and even de-reference an index to get at that data.  Consider what happens if we add a couple of keys to aFooGuard (which has a map data structure inside of it to keep track of these "right hand values":


  exclusive aFooGuard(int k1, long k2) => Foo *;

Now when we acquire the guard with a node declaration we have a chance to use some of the input arguments of the node to formulate key values:


  node Bar (InputType1 anInput1,InputType2 anInput2, 
      ...,
      guard aFooGuard(inthash(anInput1),longhash(anInput2))) 
   => (OutputType1 anOutput1,...);


The C++ functions inthash() and longhash() are not necessary, they just illustrate how we could use code buried in our C++ application to massage the inputs into appropriate keys. The semantics of an exclusive guard is that a Bar event gets exclusive access to the "right hand value" at the particular key combination it is using. For instance, one Bar event might hold aFooGuard(0,0) while another simultaneously works with aFooGuard(0,1). Finer grain keys lead to more possibilities when considering which events could run concurrently.

The decision to grab a guard does not need to be made universally for a particular node. It is possible to grab the guard conditionally on its input values:


    node Bar (InputType1 anInput1,InputType2 anInput2,...,
        guard aFooGuard(inthash(anInput1),longhash(anInput2))
            if test(anInput1)) 
     => (OutputType1 anOutput1,...);

Only if test(anInput1) returns true will the guard be acquired. The Bar C++ function is made aware of whether the guard is properly acquired and held via convenience property atoms->have_aFooGuard().

Guards can also be given precedence which will indicate to the run-time the order in which they should be acquired.  The run-time will use that ordering (complaining at compilation if the ordering is not well-formed -- since a cycle is detected in its relation) when processing node events.  All the required guards for an event need to be acquired before it can be allowed to execute.


  exclusive aBazGuard Foo * => Baz *;

  precedence aFooGuard < aBazGuard;

When grabbing multiple guards it is possible to use the "right hand value" of one guard as a key to another guard.  This sets up an implicit precedence (one that does not need to be made explicit by the programmer), and it makes the second acquisition conditional on the first having a non-NULL "right hand value":

    node Bar (InputType1 anInput1,InputType2 anInput2,...,
        guard aFooGuard(inthash(anInput1),longhash(anInput2))
            as foo if test(anInput1),
        guard aBazGuard(foo) as baz) 
     => (OutputType1 anOutput1,...);  

The syntax "as foo" lets us give a local name to the acquired guard so that it can be referenced in the key the subsequent aBazGuard acquisition. By necessity, we have to acquire the aBazGuard(foo) after foo and only when foo is not NULL.

OFlux supports (via  macros) the ability to pre-populate a guard with data before the run-time is allowed to start.  It is also possible to use similar macros to walk the guard's entire contents (all keys and values) in circumstances where you can be sure that it is safe.

Exclusive guards are just one of the acquisition semantics available within the zoo of guards.  Other semantics are available and they affect the number of node events which can hold them (at the same key-set values):


Guard Type (keyword) Pre-populated? Semantics
exclusive not needed At most 1 node event per unique keys combination
free probably no restrictions. useful for read-only cases like configuration
readwrite not needed read: only other readers allowed
write : only one node event (no readers)
upgradeable : read when non-NULL, write otherwise
pool required from a prepopulated pool for each key-set, as many node events can run as there are items in the pool

The readwrite guard is special in that a mode (read/write/upgradable) needs to be specified in the node declaration in order to control the type of acquisition that is done.  A read-only acquisition provides a const and non-reference access to the "right hand value".

Summary


Guards provide an abstraction to co-ordinate control of resources (shared data, or other things) within an OFlux program.  They support a variety of concurrent access semantics, can be acquired conditionally, pre-populated when initializing the program, and use keys similar to hash tables.  These have evolved within the history of the OFlux tool from practical use-cases that application developers had at various times. 

No comments:

Post a Comment