-
Notifications
You must be signed in to change notification settings - Fork 339
Introduction
This article introduces concepts that are essential to understand how Napa.js works. You may want to read this post on its value proposition.
In Napa.js, all works related to multi-threading are around the concept of Zone, which is the basic unit to define policies and execute JavaScript code. A process may contain multiple zones, each consists of multiple JavaScript Workers.
Within a zone, all workers are symmetrical: they load the same code, serve broadcast
and execute
requests in an indistinguishable manner. Basically, you cannot ask a zone to execute code on a specific worker.
Workers across different zones are asymmetrical: they may load different code, or load the same code but reinforce different policies, like heap size, security settings, etc. Applications may need multiple zones for work loads of different purposes or different policies.
There are 2 types of zone:
- Napa zone - zone consists of Napa.js managed JavaScript workers (V8 isolates). Can be multiple, each may contain multiple workers. Workers in Napa zone support partial Node.JS APIs.
- Node zone - a 'virtual' zone which exposes Node.js event loop, has access to full Node.js capabilities.
This complex enables you to use Napa zone for heavy-lifting work, and Node zone for IO. Node zone also compensates Napa zone on its incomplete support of Node APIs.
Following code creates a Napa zone with 8 workers:
var napa = require('napajs');
var zone = napa.zone.create('sample-zone', { workers: 8 });
Following code accesses the Node zone:
var zone = napa.zone.node;
Two operations can be performed on zones:
-
Broadcast - run code that changes worker state on all workers, returning a promise for pending operation. Through the promise, we can only know if operation succeed or failed. Usually we use
broadcast
to bootstrap application, pre-cache objects, or change application settings. - Execute - run code that doesn't change worker state on an arbitrary worker, returning a promise of getting the result. Execute is designed for doing the real work.
Zone operations are on a basis of first-come-first-serve, while broadcast
takes higher priority over execute
.
Following code demonstrated how broadcast
and execute
collaborate to complete a simple task:
function foo() {
console.log('hi');
}
// This setups function definition of foo in all workers in the zone.
zone.broadcast(foo.toString());
// This execute function foo on an arbitrary worker.
zone.execute(() => { global.foo() });
Please refer to zone
API for details.
V8 is not designed for running JavaScript across multiple isolates, which means every isolate manages their own heap. Passing values from one isolate to another has to be marshalled/unmarshalled. The size of payload and complexity of object will greatly impact communication efficiency. In Napa, we try to work out a design pattern for efficient object sharing, based on the fact that all JavaScript isolates (exposed as workers) reside in the same process, and native objects can be wrapped and exposed as JavaScripts objects.
Following concepts are introduced to implement this pattern:
Transportable types are JavaScript types that can be passed or shared transparently across workers. They are used as value types for passing arguments in broadcast
and execute
, as well as sharing objects in key/value pairs via set
and get
.
Transportable types are:
- JavaScript primitive types: null, boolean, number, string
- Object (TypeScript class) that implement
Transportable
interface - Array or plain JavaScript object that is composite pattern of above.
- Single JavaScript value undefined
Please refer to transport
API for details.
Store API is introduced as a necessary complement of sharing transportable
types across JavaScript workers, on top of passing objects via arguments. During store.set
, values marshalled into JSON and stored in process heap, so all threads can access it, and unmarshalled while users retrieve them via store.get
.
Following code demonstrates object sharing using store:
var napa = require('napajs');
var zone = napa.zone.create('zone1');
var store = napa.store.create('store1');
// Set 'key1' in node.
store.set('key1', {
a: 1,
b: "2",
c: napa.memory.crtAllocator // transportable complex type.
};
// Get 'key1' in another thread.
zone.execute(() => {
var store = global.napa.store.get('store1');
console.log(store.get('key1'));
});
Though very convenient, it's not recommended to use store to pass values within a transaction or request, since its overhead is more than passing objects by arguments (there are extra locking, etc.). Besides, developers have the obligation to delete the key after usage, while it's automatically managed by reference counting in passing arguments.
Please refer to store
API for details.