Comparison to node sync

scriby edited this page Jan 12, 2012 · 5 revisions

You may have run across node-sync and wondered how it compares to asyncblock. Both modules have similar goals, but there are some minor differences in implementation. If asked to choose between the two, the choice will likely just come down to personal preference. This article is not about bashing node-sync -- I have respect for the author for pioneering the idea of easy-to-use flow control based on fibers, and I've gained inspiration by node-sync to add specific functionality to asyncblock.

This comparison was written 1-3-2012, and attempts to be accurate as of the date of writing.

History of asyncblock

I stumbled upon fibers and a module called greenlight one day. I thought the concept of greenlight was really clever, but noticed that it was missing parallel task execution support (which would be necessary for any serious usage). As a bit of an experiment, I forked the code and extended it to allow running tasks in parallel. From there, the module just grew over time as I ran into more things I wanted to use the module for.

Why I didn't just use node-sync

At some point early on I also looked at node-sync, but something really stuck out to me:

Sync(function(){
    // Function.prototype.sync() interface is same as Function.prototype.call() - first argument is 'this' context
    var result = asyncFunction.sync(null, 2, 3);
});

I thought having the first argument specify the "this" context was quite unfamiliar. I can totally see why it's necessary with the approach taken, as the object the function call is getting made on is lost "in translation". Given that 99% of the time we don't need to maintain the "this" context, I didn't like the design trade-off. (Note that we could still use futures to maintain the "this" context even if .sync didn't support it).

Futhermore, this approach means that node-sync is adding to built-in prototypes -- something which I wanted to avoid. I will admit that in this case it's a relatively minor trade-off for a simple and always available syntax.

It was mainly those two drawbacks which convinced me to "roll my own".

Syntax comparison

Each of the following examples accomplishes the same result:

node-sync example 1

Sync(function(){
    var contents = fs.readFile.sync(null, path, 'utf8');
    console.log(contents);
});

node-sync example 2

Sync(function(){
    var future = fs.readFile.future(null, path, 'utf8');
    console.log(future.result);
});

node-sync example 3

Sync(function(){
    var future = new Sync.Future();
    fs.readFile(path, 'utf8', future);
    console.log(future.result);
});

asyncblock example 1

asyncblock(function(flow){
    fs.readFile(path, 'utf8', flow.add());
    var contents = flow.wait();
    console.log(contents);
});

asyncblock example 2

asyncblock(function(flow){
    fs.readFile(path, 'utf8', flow.add('contents'));
    console.log(flow.wait('contents'));
});

asyncblock example 3

asyncblock(function(flow){
    var contents = flow.sync(fs.readFile, path, 'utf8');
    console.log(contents);
});

asyncblock example 4

var fs = asyncblock.wrap(require('fs'));
asyncblock(function(flow){
    var contents = fs.sync.readFile(path, 'utf8');
    console.log(contents);
});

asyncblock example 5

var fs = asyncblock.wrap(require('fs'));
asyncblock(function(flow){
    var future = fs.future.readFile(path, 'utf8');
    console.log(future.result);
});

In the first two node-sync examples, you have to specify an extra first argument. In the third one, you need an extra line to declare the future (example on the node-sync readme shows this with an inline assignment).

I tried to provide as succinct as possible syntax with asyncblock to make sure the flow control library stays out of the way as much as possible. The first example is the most basic syntax, and it builds from there. The only asyncblock example that loses the "this" context is the 3rd one.

Difference comparison

Modifies built-in prototypes?

  • asyncblock: No, and it never will. We achieve similar functionality by providing the asyncblock.wrap syntax. It's a little more verbose, but it also keeps the "this" context in tact on the calls made.
  • node-sync: Yes. The functions sync, future, async, and asyncMiddleware are added to the Function prototype. I don't see a strong argument for adding async and asyncMiddleware -- those could have been exposed as normal functions.

Above series & parallel execution, what features are there?

  • asyncblock: Timeouts (fatal & non-fatal), max parallel throughput, converting multi-valued callback results to a map, manually handle errors for certain tasks, capturing more stack trace information, prevent exception bubbling, wait on all outstanding tasks
  • node-sync: Timeouts (fatal), capturing more stack trace information, built-in sleep syntax, async & asyncMiddleware (convert sync style functions into async style), repl (I can't tell exactly what this does), wait on all outstanding tasks

Some of the features are simple to implement and aren't an important distinguishing factor. Some features may be troublesome, like converting multi-valued callback results, as I don't think node-sync has a good place to specify the response format.

Performance comparison?

The bulk of the work is being done in the node-fibers module, so the performance should be similar between the two libraries. In my preliminary testing asyncblock is a little faster than sync, but the difference is not a likely deciding factor.

  • asyncblock: I've run it with the profiler and optimized out hot spots & memory leaks.
  • node-sync: I'm not sure how much attention the author has paid to performance.

How about error handling?

  • asyncblock: Specify your exception handler by setting flow.errorCallback. If an exception / error occurs in the asyncblock, the errorCallback will be called. In general, you just set the errorCallback to your current function's callback and you don't have to think any more about it.
  • node-sync: Specify a result handler as the 2nd argument to the Sync block. If err is set, an exception / error occurred in the Sync block.