Skip to content

AboutSynchronization

JeanHuguesRobert edited this page Feb 15, 2013 · 17 revisions

Definitions

Synchronization is about doing things in the right order.

The most basic synchronization is about waiting for the result of some asynchronous function. Such a function usually accept a "callback" parameter. That parameter is a function that will be called when the result of the asynchronous function is available.

In the most basic sense, "to block" means to wait for such an asynchronous result. When the result is available, execution can resume and the next step is executed.

The "callback" pattern is convenient and well known. There exists another pattern that is very convenient too. It's the "promise" pattern (sometimes called "future" or "deferred"). With that pattern, an asynchronous function uses an intermediary object to provide a result. That object, the promise, is first returned by the asynchronous function and is later "resolved" when a result is available. If that result is an error, the promise is rejected instead of resolved.

When it gets the promise returned by the asynchronous function, the caller can register a callback to be called when that promise is resolved.

function timeout( delay ){
  var promise = l8.promise()
  setTimeout( function(){ promise.resolve() }, delay );
}

var wait_10_ms = timeout( 10 );
wait_10_ms.then( function(){ console.log( "10 ms later" ); }

Because l8 knows about promises, when the result of a step is a promise, l8 blocks the execution of the current task until that promise is either resolved or rejected. When the promise is eventually resolved, execution is resumed and the next step is executed. If the promise is rejected, the rejection reason is handled as an exception.

l8.step( function(){ return timeout( 10 );
}).step( function(){ console.log( "10 ms later" ); })

There is another solution to achieve a similar effect, ie pausing and then resuming execution. It is about explicitly pausing and resuming the current task to block/deblock it, instead of waiting for a promise or a callback.

function timeout( delay ){
  var task = l8.current
  task.pause()
  setTimeout( function(){ task.resume() }, delay );
}

l8.step(){ function(){ timeout( 10 );
}).step(){ function(){ console.log( "10 ms later" ); })

The difference in usage is small. In the promise case, the promise was the returned value of the step. Whereas no value is returned in the pause/resume case, the pause happens as a side effect.

There are pros/cons for each solution: the callback style, the promise style, the blocking style.

l8 was designed so that it is easy to intermix these styles and use whichever style is the most appropriate for the task at hand. For that purpose, l8 synchronisation objects, described later on, all provide a .promise accessor to use when the blocking style is inconvenient and the promise style is preferable.

Similarly, all promises have a .callback accessor that returns a node js style callback, ie fn( err, rslt ), that, when called, will either resolve or reject the promise.

To make things actually even more easy to use, all l8 synchronization objects provide a .then() method and both a .promise and a .callback accessor.

Blocking

Steps are useful to describe flows that depends on other flows. As a result a step often involves sub steps or sub tasks. The step then "block" waiting for the sub items to complete.

For simple steps, that only depend on the completion of a simple asynchronous function, l8.walk or l8.proceed() provides the callback to register with that function.

When that callback is called, execution that was paused on the current step is resumed and the next step is executed.

Note: in the frequent case where the callback only needs to store the result of the asychronous operation and move forward to the next step, please use "l8.walk" instead of l8.proceed( function( x ){ this.result = x }).

  l8.step( function(){ setTimeout( l8.walk, 10 ) // block for 10 milli seconds.
  }).step( function(){ console.log( "10 ms later" ) })

However, if the action's result dictates that some new "nested" steps are required, one adds new steps from within the callback itself. Often, this style of programming is not the best because it basically resolves back to the infamous "callback hell" that l8 attempts to avoid. A better solution is to let the next step handle that.

Do:

  @step           -> fetch                      @walk
  @step( result ) -> if result then more_fetch  @walk
  @step( result ) -> if result then fetch_again @walk
  @step( result ) -> if result then use result  @walk
  @step           -> done()

Don't:

  @step -> fetch @proceed (result) ->
    if result
      more_fetch @proceed (result) ->
        if result
          fetch_again @proceed (result) ->
            if result then use result @walk
  @step -> ...

Or, even better, use task constructors instead of functions with callbacks:

  @step      -> fetch()
  @step( r ) -> more_fetch()  if r
  @step( r ) -> fetch_again() if r
  @step( r ) -> use r         if r

Note: please use l8.Task( fn ) to define a task constructor.

Synchronization objects

A synchronization object is an object that is useful to... synchronize things. l8 provides many types of synchronization objects. Which one to use depends on what one wants to achieve.

All synchronization objects share a common protocol, ie their usage is similar, even when their semantic changes:

  • l8.wait( obj ) will pause the current task until the designated object is "signaled".
  • obj.signal( ... ) will "signal" the designated object.
  • obj.cancel() will "cancel" the designated object.
  • obj.then( fn_ok, fn_ko ) will invoke fn_ok when the designated object is "signaled" or will call fn_ko when the designated object is "cancelled".
  • obj.promise provides a promise that is resolved when the object is signaled or rejected when the object is cancelled.
  • obj.callback provides a fn( err, rslt ) function that, when called, will either signal or cancel the object.

Synchronization objects includes Tasks, Promises, Semaphores, Mutexes, Locks, Ports, Message queues, Generators, Timeouts, Calls, Stages and Actors.

Let's see what "signal" and "cancel" means for each such objects.

Tasks

A task is very much like a function call. That is to say that "calling a function creates a function call". The same is true for task: "Calling a task constructor creates a task". Task constructors are to tasks what functions are to function calls.

A function call, somewhere inside the javascript interpretor, is an object. An invisible object. That invisible object has invisible attributes! One that is very obvious, yet difficult to access, it the so called "caller". The caller of a function call is another function call ; the one that created it, the one that called the function call's function. The "function" of a function call is also very obvious, it is the function that was called. It is made of javascript script statements. When a function is called, it is these statements that are executed, as specified in the function's definition.

There is a chain here, with function calls created by previous function calls, their "caller". That chain is the "call stack", so called a stack because "functions returns", or, to be slightly more accurate "function calls return". They return "a result" usually, sometimes the special "undefined" one. When an error occurs, functions return their result as an exception.

When one looks at that stack, only one function call is ever the "active one": it is the last one, the one at the top of the stack. The other calls will become the active one eventually: the caller of the active call will become the active one when a current active call will "return". Then it will be the caller of that caller call turn, and so on, until control returns to the top most function call, the one that runs the javascript event loop.

In a way, all callers are "blocked", waiting for their "callee" to return. When that callee returns a result, the caller is deblocked. This may come as a surprise but, at some level, all javascript functions block! They wait for their callee to return.

Tasks are similar to function calls. There are some differences however. The "caller" of a task is called it's "parent". It's "callees" are called it's "sub tasks". And, of course, task don't execute javascript statements, they execute "steps" instead.

The stack of function calls is just that, a stack. Tasks are different. While a function call can only create another function call and then wait, until that callee return, a Task has additional powers. It can create multiple sub tasks, not just one, and wait until they all return. Among the sub tasks, none is privileged, none is the "active" one. As a result, there is not a stack of calls, there is a tree of tasks.

Tasks wait for their sub tasks, that's convenient. But they can do otherwise. They can do other stuff instead of passively waiting. They can create additional sub tasks for example.

Let's have an example, an http server. It is made of two components. One is a node.js http server. The other one is a task that handles requests coming from a queue that the server fills.

var queue = l8.queue();
http.createServer( queue.put.bind queue ).listen( 80 );
l8.task( function(){
  l8.repeat( function(){
    var response;
    l8.step( function(          ){  queue.get();
    }).step( function( req, res ){  response = res;
                                    l8.fs.readFile( "msg_of_the_day.txt" );
    }).step( function( content  ){  response.end( content ); })
 })
})

This is fine. But there is a catch. Requests are processed in a pure sequential order. If "readFile" takes a long time, other requests are blocked in the queue. In such situations, it is better to create an independent sub task, it is best to "spawn" a new task. As a result there will be one new task for each new request.

var queue = l8.queue();
http.createServer( queue.put.bind queue ).listen( 80 );
function serve(){ l8.fs.readFile( "msg_of_the_day.txt" ); }
var main_task = l8.task( function(){  l8.repeat( function(){
    l8.step(  function(){  queue.get();
    }).spawn( function( _, response ){
      l8.step( function(   ){  serve();
      }).step( function( r ){  response.end( r ); })
    })
  })
})

This is a very simple http server. It always serve the same text file. But things could be more complex, there is no limits.

var serve = l8.Task( function(){
  var prelude;
  l8.step( function(   ){  l8.fs.readFile( "prelude.txt" );
  }).step( function( r ){  prelude = r;
                           l8.fs.readFile( "main.txt" );
  }).step( function( r ){  return prelude + r; })
})

serve(), a task constructor, now creates a sub task that returns the content of two files. What serve() does has changed, but the body of the server is still the same, it still calls serve() as before.

This works because l8 tracks what is the current task and knows that when it is done, it's result shall be provided to the caller task. The javascript interpretor does the exact same thing, ie when a function returns, it's result is provided to the caller, the calling function call.

So, this is it, the very basic synchronization mechanism where a task and it's sub task synchronizes.

But there is more. Any task can monitor what is happening to any other task. For example, what happens if the http server fails, how can that event be detected?

l8.spawn( function(){
  l8.step(                 main_task )
  .failure( function( e ){ console.log( "main task error",   e ); })
  .success( function( s ){ console.log( "main task success", s ); })
})

What this code does is it spawns a new task that waits for the outcome of the http server main task and then logs a message about the outcome of that task.

There is some sugar in that solution. There is another solution, almost identical.

l8.spawn( function(){
  l8.step(    function(   ){  l8.wait( main_task );
  }).failure(   function( e ){  console.log( "main task error",   e );
  }).success( function( s ){  console.log( "main task success", s ); })
})

This time, l8.wait() is involved. Whereas in the previous solution waiting for the main task outcome was implicit, because a step made of a reference to task means "wait for that task", this time the step is explicitly described: it must wait for the main_task to be signaled.

So "a task is signaled when it's outcome is a success". What about "cancel"?

main_task.cancel();

This code "cancels" the main task. When that happens, it is like if an exception had occurred within the task, a "cancel" exception. As a result (because errors where not caught in the example), the task fails. That is a brutal way to terminate a task. Not only does the task fails, but all its sub tasks fail too.

Fortunately, when a task is "cancelled", it can catch that event and terminate itself in a clean way. Doing this involves using l8.failure() within the task.

Promises

Semaphores

Mutexes

Mutexes are useful to provide access to critical resources, resources that multiple activities must not use concurrently but must use in sequence instead.

var mutex = L8.mutex()
  ...
  .step( mutex )   // or .step( function(){ l8.wait( mutex ) })
  .step( function(){
    l8.defer( function(){ mutex.release() })
    xxx
  })
  ...

Locks

Lock are like Mutexes but when a task that acquired a resource tries to acquire it again... it is not blocked, whereas it would be block if it had acquired the resources using a Mutex. Hence locks are sometimes called "reentrant mutexes", because a task can re-enter a critical section, it will not be blocked.

Message queues

Ports

Ports are like message queues, except there is no queue! As a result, when a task wants to put something in such a queue without a queue, it must block until another task is doing the opposite.

Generators

Generators are sub tasks that provide a result in multiple pieces instead of in just one piece as regular tasks do.

Each such a task is a "producer" of results and another task, often the one that spawn the generator, is called "the consumer" of these results.

Consumers consume the next result that some sub task yields. This usually happens until the generator reaches an end and is closed, either by the producer or the consumer.

l8.Generator( block) builds a "Generator Constructor" much like l8.Task( fn ) does with "Task Constructorf". When the constructor is called, a generator task is spawn and a reference to it is returned.

That task uses .yield() to produce results. On the consumer side, the task uses gen.next( [opt] ) to get that result and maybe provide a hint about future results.

  var fibonacci = l8.Generator( function(){
    var i = 0, j = 1;
    l8.repeat( function(){
      l8.yield( i);
      var tmp = i;
      i  = j;
      j += tmp;
    })
  })

  var gen = fibonacci()
  var count_down = 10
  l8.repeat( function(){
    l8.step( function(){
      if( !count_down-- ) l8.break
      gen.next()
    }).step( function( r ){
      trace( count_down, "fibonacci: " + r)
    })
  })

Timeouts

Calls

Stages

Stages and actors are covered in details in the next chapter.

There is little synchronization to do with stages. The two main events are about contact with a stage, first when it is established, then when it is lost.

Contact is always established at the initiative of one of the two peers involved. When one of them is a browser, it is the one initiating the contact ; there is no choice, the browser knows the url address of the server, the opposite is generally false.

A stage is signaled when the contact is established.

To monitor the loss of contact, please use stage.defer()

As expected stage.cancel() will close the communication channel with the designated stage, ie contact is voluntarily lost.

Actors

It is possible to monitor actors like tasks. An actor gets signaled when it dies, either gracefully or because of some failure.

When an actor is accessed remotely, thru a proxy, it is the connection with the actor that gets monitored, not the actor itself.

To cancel an actor is to cancel it's underlying task. For proxies, it just mean that the contact get lost.

Next chapter: Actors and Proxies. Index.