Friday, August 3, 2012

OFlux Multipling Successor Events

Dynamically increasing the number of output successor events from a given node event execution is a powerful concept. By submitting many events to the OFlux run-time event queue, we can distribute the follow on work to other run-time worker threads. In a previous post, I described how to (staticly) have a node event's output processed concurrently by two separate nodes. Although similar, the functionality of processing the same input with two C++ node functions in the flow is orthogonal to the idea that a node event might produce several outputs.

Producing No Output


If a node function wants to cancel the flow to its successors it can do that by returning a non-zero result. This -- in effect -- means that the execution of that node function encountered an error. If there is an error handler for that node, it will be called -- but passed the input to the node that threw the error. The error handler node has a chance to re-inspect the input to the failed node function, and take remedial action:


Node Foo (const char * type) => (int type_id);
Node Oops (const char *) => ();

handle error Foo -> Oops;

The C++ code for the Foo node might check a static look-up table for a matching entry and return an error when no entry is found (causing no successor events to run, but rather having an Oops node event process the input instead):

int
Foo(const Foo_in *in, Foo_out *out, Foo_atoms *)
{
  static struct { const char * t, int tid } lookup[] =
    { { "apple", 1 }
    , ...
    , { 0, 0 } };
  int res = -1; // indicates not found - its an error
  for(size_t i = 0; lookup[i].t; ++i) {
    if(0 == strcmp(lookup[i].t,in->type)) {
      res = 0;
      out->type_id = lookup[i].tid;
      break;
    }
  }
  return res;
}


If no error handler is declared for a node, then no error node event is scheduled to run (meaning the error is ignored).

Producing More Output


In order to have a node produce more than a one output structure (leading to many successor events), there is a C++ help er gadget called oflux::PushTool<> which gives the node function access to this capability. Here is an instance of its use:



Modifying the flow above to have the Foo node "splay" all matching outputs in the lookup table (rather than just the first one) by adding a (non-compulsory) comment:

node Foo(const char * type) => /*splay*/ (int type_id); 

And changing our C++ implementation of Foo as follows:

Foo(const Foo_in *in, Foo_out *out, Foo_atoms *)
{
  static struct { const char * t, int tid } lookup[] =
    { { "apple", 1 }
    , ...
    , { 0, 0 } };
  size_t out_count = 0;
  oflux::PushTool<Foo_out> ptool(out);
  for(size_t i = 0; lookup[i].t; ++i) {
    if(0 == strcmp(lookup[i].t,in->type)) {
      ++out_count;
      ptool->type_id = lookup[i].tid;
      ptool.next();
    }
  }
  return out_count ? 0 : -1;
}

Now Foo could produce several outputs (type_ids) on one input, and each of those will get processed using the flow that follows node Foo. If no matches are found in the lookup, then no flow will operate (no successor node events).

Summary


In some cases where it is advantageous to release multiple outputs from a node (causing more successor events to be created), the run-time can be leveraged to dispatch the processing of those events on multiple threads. More threads doing work means more concurrency. The trade-off is that the successor event work must be more (in single thread terms) than the over head incurred (in the run-time) by doing this. It may be that successor processing happens so quickly in a single thread, that having Foo just output a container (e.g. vector) containing all results is better.

No comments:

Post a Comment


Follow Mark on GitHub