Skip to content
Jean Hugues Robert edited this page Jan 24, 2014 · 23 revisions

l8 is a pthread/erlang inspired multi-tasker for javascript. Multi-tasking is about running multiple tasks with some parallelism instead of running them in sequence. See http://en.wikipedia.org/wiki/Computer_multitasking

A multi-tasker is typically a tool to transform a single flow of execution and multiply it, ie typically a singleton stack of function calls is transformed into multiple threads of control, ie multiple stacks of function calls.

Because javascript is single threaded by design, it is impossible to have multiple stacks of function calls, the function call stack of the javascript event loop is the one and only stack available. ECMAScript 6 does introduces multiple call stacks with yielding generators, a special purpose type of co-routines ; it is not yet widely available however.

As a consequence of this lack of multiple stacks of calls, l8 manages the flow of execution at a higher level, not the stacked function calls level. That higher level is the "step" level. At that level, stacked function calls become stacked "tasks" and tasks are made of steps much like function calls are made of statements.

Note: it is possible to "simulate" the existence of multiple stacks of function calls by implementing a "dialect" of javascript, with additions to handle stacks switching. This basically involves the classical compiler->generation->execution tool chain. The result is not pure javascript, however close. Usually the performance penalty is significant because it is to some extend like implementing a virtual machine on top of the javascript machine.

Steps - l8.step(), l8.walk, l8.proceed()

Steps are to tasks what statements are to javascript function calls. What does it mean in practice?


var aaa = function(){ console.log( "aaa" ); }
var bbb = function(){ console.log( "bbb" ); }

var fff = function(){
  aaa();
  bbb();
};

var ttt = l8.Task( function(){
  l8.step( function(){  aaa();
  }).step( function(){  bbb();
})});

...
fff(); // outputs aaa and bbb
ttt(); // this does the same

fff() is a function, made of 2 statements. ttt() is a task constructor, for tasks with two steps. l8 schedules the execution of steps like javascript does with statements. The major difference is that javascript statements cannot block whereas l8 steps can.

var aaa = function(){
  l8.step( function(){  setTimeout( l8.walk, 10 );
  l8.step( function(){  console.log( "aaa" ); })
}
...
ttt(); // will output aaa... within 10 ms

In this example, aaa() is redefined to involve two steps. One that blocks for 10 milli-seconds and another one that outputs "aaa". The first step blocks because it references l8.walk. That reference is detected by l8 because .walk is an attribute defined using _defineGetter(). When the value of l8.walk is called (it is a callback, setTimeout() expect a callback), the task is deblocked. This happens 10 milli-seconds later because setTimeout( cb, delay ), a javascript native function, was designed to do that: call the function specified with the cb parameter after the specified delay.

l8.step() defines what are the steps composing a task. l8.repeat() and l8.fork() define special steps that handle loops and sub tasks. They are described in details in the documentation.

Promises - l8.promise(), .resolve(), .then()

Promises are a pattern where the result of an async function call is provided thru an intermediary object, a promise, instead of being delivered using direct callbacks.

Callbacks are still involved, but indirectly, they are attached to the promise object.

When the result of the async function call is available, that function "resolves" a promise instead of calling a callback.

When the promise is resolved, callbacks attached to it are called.

This pattern is convenient to compose async function calls.

function timeoutPromise( delay ){
  var promise = l8.promise();
  setTimeout( function(){ promise.resolve(); }, delay );
  return promise;
}
...
timeoutPromise( 10 ).then( function(){ console.log( "10 ms later" ); } );
...
l8.step( function(){  return timeoutPromise( 10 );
}).step( function(){  console.log( "10 ms later" ); })

Note: l8 tasks are also "promises". Once a task is completed, it's promise is either fulfilled or rejected depending on the task success or failure.

Tasks - l8.spawn(), l8.repeat(), .failure()

l8 schedules the execution of multiple "tasks". A task is made of "steps", much like a function call is made of statements. Steps are closures, ie tasks are made of steps made of statements.

Execution goes from "step" to "step". The output of a step becomes the input of the next one.

l8.step( function(      ){  return [ "hello", "world" ];
}).step( function( h, w ){  console.log( h, w ); } );

If one cannot walk a step immediately, ie if a step involves an async result, one does block, waiting for that result before resuming.

l8.step( function(){
  // block task for 10 ms and then provide a result for next step
  setTimeout( l8.proceed( function(){ return [ "hello", "world" ] }, 10 );
}).step( function( h, w ){
  console.log( h, w );
})

Tasks can nest like blocks of statements (try/catch style). They can also create "spawn" independent new tasks.

l8.spawn( function(){
  l8.task( function(){
    fff();
    l8.step( function(){
      ttt()
    }).failure( function( err ){ l8.trace( "! some issue with ttt()", err ); })
  }).failure( function( err ){ l8.trace( "! some issue with fff()", err ); })
})

l8.spawn( function(){
  l8.repeat( function(){
    console.log( "hi" );
    l8.delay( 10 * 1000 );
  })
})

Hence l8 tasks are kind of user level non preemptive threads. They are neither native threads, nor worker threads, nor fibers nor the result of some CPS transformation. Just a bunch of cooperating closures. However, if you are familiar with threads or fibers, l8 tasks should seem natural to you.

Synchronization - l8.signal(), l8.wait(), l8.queue()

The main flow control structures are the sequential execution of steps, the execution and join of forked steps on parallel paths, error propagation similar to exception handling and synchronisation using the usual suspects (semaphores, mutexes, reentrant locks, message queues, ports, signals, generators...).

var queue = l8.queue();
var start = l8.signal();
l8.spawn( function(){
  l8.step( function(     ){  l8.wait( signal );
  }).step( function(     ){  queue.get();
  }).step( function( msg ){  console.log( msg ); })
})
l8.spawn( function(){
  start.signal();
  l8.repeat( function(){
    l8.step( function(){  l8.delay( 100 );
    }).step( function(){  queue.put( "it's alive!" ); })
  })
})
```

These tools are further described in this documentation.

Actors - l8.actor()
===================

The "thread" model of computation is not without shortcomings however. Race conditions and deadlocks are difficult to avoid when using the shared state paradigm.

What is sometimes a necessary evil to gain maximal performance out of multiple cores cpus is not an option within a javascript process that is by design single threaded.

This is why l8 favors a different approach based on message passing and distributed actors.

Message passing is a solution to isolate processes in order to avoid issues with shared states, including synchronization issues such as the race conditions and deadlocks already mentioned. See http://en.wikipedia.org/wiki/Message_passing

Actors is programming model where active objects, called actors, communicate the one with the other using message passing. See http://en.wikipedia.org/wiki/Actor_model

In addition to the intrinsic advantages of this elegant solution, actors also solve a very real issue with javascript: the inability to benefit from multi-core cpus. Javascript cannot use more than one core. It cannot do that because there is only one thread of control, only one stack of function calls.

Fortunately, it is often possible to have multiple javascript processes that run in the same machine. In that situation, when a javascript process needs to execute some statements, the first available core will be allocated to the javascript process. When multiple javascript processes are concurrently active, multiple cores will process the statements. Performance will increase when the number of cores increases.

There is however an overhead associated with message passing. It's expensive. When a message passes a process frontier, that message must be copied on the other side, inside the receiver process. Passing a message is much more expensive than passing parameters when calling a function.

At some point, the advantages of multiple cores overweight the overhead of message passing. With the number of cores now increasing much faster than the speed of each core, the future for the actor model is bright.

l8 implements actors using multiple methods. When running in a node.js server, l8 leverage the "clustering" solution provided by node.js. When running in a browser, l8 leverage the "worker thread" API that modern browsers provides. In both cases, when performance matters, adding more machines is also possible, using message passing over some communication protocol (tcp, http, WebSocket...). Note: these features are not available yet (jan 2014).

Additionally, to the extend possible, l8 actors don't need to be coded with any knowledge regarding how they communicate with their peer actors. The same l8 API is available in all the cases, only the "address" of the actors, aka their "name", matters. For remote actors, that name may include an url.

```
var publisher = l8.actor( "pub",      "http://machine_a.mydom.com"    );
var broker    = l8.actor( "exchange", "http://machine_x.theirdom.com" );
l8.repeat( function(){
  l8.step( function(   ){ l8.delay( 1000 );
  }).step( function(   ){ broker.ask( "quote", "the actor model" );
  }).step( function( q ){ if( q > 10 ) publisher.tell( "story", "the actor model is cool!" ); } )
})
```

Defining an actor is easy too.

```
l8.actor( "pub", function( verb, param ){
  if( verb === "story" ) console.log( param );
});

l8.actor( "exchange", function( verb, model ){
  if( verb === "quote" ){
    if( model === "the actor model" ) return 20;
  }
  return 0;
}
```



Next chapter: [Tasks](AboutTasks). [Index](FrontPage).

Clone this wiki locally