New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

workers: initial implementation #1159

Closed
wants to merge 389 commits into
base: v1.x
from

Conversation

Projects
None yet
@petkaantonov
Copy link
Contributor

petkaantonov commented Mar 15, 2015

Workers are light-weight processes that are backed by OS threads.

This is an initial implementation that is behind the flag --experimental-workers. It should have very little effect on other io.js components, even if the flag is enabled. The module can be required by doing var Worker = require('worker'); - see how the tests do it in test/workers folder.


What is currently implemented:

Worker constructor which takes an entry module and an optional options object. Currently (I have some ideas for more) the only option is keepAlive:

var worker = new Worker('worker.js', {keepAlive: false})

The option defaults to true, which means the worker thread will stay alive even when its event loop is completely empty. This is because you might not have
anything to send to the worker right away when a server is started for instance.

A worker object is an event emitter, with the events 'message', 'exit', and 'error' and public methods postMessage, terminate, ref and unref:

  • 'message' event will receive currently a single argument, that is directly the message posted from the worker thread.
  • 'exit' event is emitted when the worker thread exits and receives the exit code as argument.
  • 'error' event is emitted when there is an uncaught exception in the worker which would abort a normal node process. The argument is the error thrown,
    if it's an error object, its builtin type, stack, name and message are retained in the copy. It will additionally contain extra properties that were in the
    original error, such as .code.
  • postMessage([message]) where message is structured clonable* can be used to send a message to the worker thread. Additionally internal messaging support is implemented
    that doesn't need to copy data or go through JS. (* currently implemented as JSON to keep PR small, more details later)
  • terminate([callback]) terminates the worker by using V8 TerminateExecution and uv_async_send, so the only thing that
    can defer a worker's termination is when it is executing C code which shouldn't contain any infinite loop or such :-).
  • unref and ref work like timer's respective methods, however keep in mind that a worker will terminate if its owner terminates.

Inside a worker thread, several process-wide features are disabled for example setting the process' current working directory, title, environment variables or umask. Workers can still read these, but only the main thread can change them. Otherwise all core modules and javascript works normally inside a worker, including console.log.

Inside worker thread, the Worker constructor object is an event emitter, with the event 'message' and the method postMessage() to communicate with its owner (see tests for usage). The worker constructor can be used to construct nested workers with the worker being their owner. Passing messages from grand child to grand parent requires the child's parent to pass its message through, however transferable message ports can be implemented if use cases emerge.

Nested workers were implemented because Web Workers support them too (except Chrome) and I envision that a setup with ${CPU cores} amount of web servers with N amounts of sub-workers for each will be very useful.

2 new public process read-only properties are introduced process.isMainInstance and process.isWorkerInstance.


Advantages of workers over processes:

  • Internal ITC (not passing JS objects) is ridiculously fast with the wait-free message channel
  • User ITC is probably faster as well, not having the overhead of JSON parsing and serialiazing
    • And can also transfer more kinds of objects, such as Dates, Maps, Sets, objects with circular references...
    • Additionally ownership transfer of external resources like ArrayBuffers and external strings can be done without copying
    • Ownership transfer of uv handles will probably require support from uv so that handles can be detached from an event loop
      and adopted by another.
  • Threads have less context-switch overhead, making it feasible to run many more workers than there are CPU cores
    which is more graceful in high load situations. And if we implement worker-cluster, there shouldn't be the need
    to use home-made round-robing scheduler.
  • Free of problems like process.send not always being synchronous
  • There is just one process to kill and no runaway processes

Disadvantages:

  • Cannot rely on OS to free resources upon worker exit. However the architecture that
    enables freeing all resources upon Environment destruction is there and
    is already used to free all resources that a new worker that doesn't do
    anything but exit immediately allocates.
  • C code maintainers need to think about thread-safety. However, there is generally
    little amount of shared resources as most stuff is tied to a v8 isolate or a uv event loop
    which are exclusive to a worker.

Compared to traditional threading:

  • Workers are expensive to create, one does not simply spin up a worker to do 1 task and throw it away.
  • Workers cannot share any JS memory (this is impossible in V8), to JS users they appear pretty much as isolated as real processes

To keep the PR size small (believe it or not, deep copying will take much more code than workers alone), objects are still copied by JSON serialization. I want to implement something like the structured clone with some differences as the spec has some seriously nasty parts that are not really needed in practice but force the implementation to be so slow as to defeat the whole point of using it in the first place.

So what cannot be implemented from the algorithm:

  • The html5 objects like File (since we don't have those)
  • Named properties of Arrays (inconsistent and extremely nasty to implement using v8's api)
  • (implicit) If you have sparse array and define indexed properties in protototypes, then those values in prototypes are used in the resulting array. V8 has internal check that array and object prototypes are clean of indexed properties, but we don't have access to that.

What could be implemented but don't want to implement from the algorithm:

  • Normal properties of Map and Set. This would be inconsistent and add more stuff to implement, but certainly doable.

What I mean by inconsistent is that copying map, set and array properties is inconsistent because the properties of Date, RegExp, Boolean, Number, and String objects are dropped.


To make reviewing this easier, a description of changes in each file follows:

  • *test/workers/**_, _tools/test.py, Makefile
    • For testing. Run make test-workers. Worker tests are not ran as a part of make test
  • worker.cc, worker.h, worker.js
    • Majority of the worker implementation is here
  • util.h, util-inl.h, util.cc
    • Added STATIC_ASSERT
    • Added some threading helpers
    • Added a hack to get a char* from js string (TODO)
    • Added Remove method to ListHead
  • smalloc.cc, smalloc.h
    • Extracted CallbackInfo class declaration to a header
    • Moved ALLOC_ID to env.h
  • producer-consumer-queue.h
    • Modified folly's wait-free single-producer single-consumer queue.
  • persistent-handle-cleanup.cc, persistent-handle-cleanup.h.
    • Because finalizers are not guaranteed to be called even when a v8 isolate is disposed, these implement PersistentHandleVisitor that is used to walk all unfinalized weak persistent handles to release their C resources upon Environment destruction
  • notification-channel.cc, notification-channel.h
    • These work around uv_async_send unreliability when async handles need to be unreffed
  • node-internals.h
    • Removed NodeInstanceData - the event loop code for worker became too different from the main thread event loop code for this to be useful
  • node-contextify.cc
    • Extracted out ContextifyScript class declaration to a header. The diff looks horrible but there is no real changes
    • Added check to prevent cancellation of termination in EvalMachine method
  • node.js
    • Added worker mode startup and cleanup registration for stdio and signal handles. (environment->CleanupHandles() is actually unused by main instance, but workers use it)
  • node.cc
    • Added locking around process-wide methods. (If uv's rwlock still causes deadlocks in Windows 2003, it should be changed to mutex).
    • Added worker-only locking for module initialization, the ApiLock() is null for main instance but workers need it because module initialization isn't termination safe.
    • Made MakeCallback safe to call inside workers that are being abruptly terminated
    • Changed all exit calls to env->Exit() (which only exits the process if called from main thread)
    • Added an export for process._registerHandleCleanup()
    • Moved atexit from LoadEnvironment to Init (probably not the right place, but it cannot be in LoadEnvironment)
    • Added CreateEnvironment overload to be called from worker threads
    • Extracted Environment initialization to InitializeEnvironment
    • Removed all worker checks from StartNodeInstance and renamed it to RunMainInstance. Added a call to TerminateSubWorkers() before exit event is emitted.
    • Added ShutdownPlatform and delete platform at the end of Start method.
  • handle_wrap.cc, handle_wrap.h
    • Added a way to register handle cleanups upon Environment destruction.
  • env.h, env-inl.h
    • Introduced a ClassId enum which weak persistent wrappers can use so that they can be walked upon Environment destruction and cleaned up for sure.
    • Added some worker related methods
    • Added set_using_cares(), which means that in Environment destructor destroy_cares is called
    • Added CanCallIntoJs() method: main instance can always call into js but workers can be in the middle of termination in which case one cannot call into js anymore
    • Added Exit method which does process exit if called from main thread and worker termination if called from worker thread
  • cares_wrap.cc
    • Added InitAresOnce to do one-time process-wide initialization
    • Added a call to env->set_using_cares() (see env.h above)
  • async-wrap.cc
    • Made same changes to MakeCallback as were made to node::MakeCallback
  • timers.js
    • Added handle cleanup registrations for timer handles so that when a worker Environment is destructed, the handles won't leak
  • child_process.js
    • Added handle cleanup registration for the pipe handle so that when a worker Environment is destructed, the handle won't leak. The other handles probably leak too but this was the only one that is caught by current tests. There is currently an assertion in worker termination code for leaked handles.
  • LICENSE
    • Added ProducerConsumerQueue's license
@mscdex

This comment has been minimized.

Copy link
Contributor

mscdex commented Mar 15, 2015

What exactly is meant by "Named properties of Arrays?"

}, null, WorkerContextBinding.ERROR);
};

require('util')._extend(Worker, EventEmitter.prototype);

This comment has been minimized.

@mscdex

mscdex Mar 15, 2015

Contributor

Can this reuse the existing cached require('util')?

@petkaantonov

This comment has been minimized.

Copy link
Contributor

petkaantonov commented Mar 15, 2015

Property that is a string or symbol as opposed to an integer (which are called indexed properties)

@domenic

This comment has been minimized.

Copy link
Member

domenic commented Mar 16, 2015

Epic feature :D.

Regarding structured clone, @dslomov-chromium had plans to bring that into the ES spec and presumably also into V8 directly. I am not sure what the status of that is though. It would be good to collaborate to avoid future back-compat breakage if we do ever get the chance to switch to V8's structured clone.

@bnoordhuis

This comment has been minimized.

Copy link
Member

bnoordhuis commented Mar 16, 2015

That's the ES7 typed objects strawman, isn't it?

@vkurchatkin

This comment has been minimized.

@petkaantonov

This comment has been minimized.

Copy link
Contributor

petkaantonov commented Mar 16, 2015

@domenic Yea, the thing should have been implemented in V8 to begin with. Firefox for example implements it in SpiderMonkey, with callbacks for host objects.

Another solution can be patching V8 with some API additions that are already possible in v8 but just not done. E.g.:

  • Array::HaveIndexedPropertiesInPrototypeChain
  • Array::IsSparse()
  • Object::GetOwnRealNamedProperties()
  • Object::GetRealIndexedOwnProperty

Also what would be nice to have:

  • Object::GetRealNamedOwnProperty

But of course most optimal solution is implementing structured clone in v8, the above additions would just enable avoiding completely ridiculous things like having to call GetPropertyNames() on an array.

@bnoordhuis

This comment has been minimized.

Copy link
Member

bnoordhuis commented Mar 16, 2015

Object::GetRealNamedOwnProperty

You should be able to emulate that like this:

Local<Object> object = /* ... */;
Local<String> key = /* ... */;
Local<Value> property = object->GetRealNamedProperty(key);
const bool is_own_property =
    !property.IsEmpty() && object->GetRealNamedPropertyInPrototypeChain(key).IsEmpty();

Array::HaveIndexedPropertiesInPrototypeChain

Local<Array> array = /* ... */;
const uint32_t index = /* ... */;
const bool is_from_prototype =
    !array->HasRealIndexedProperty(index) &&
    !array->HasIndexedLookupInterceptor() &&
    array->Has(index);

(Or just check HasRealIndexedProperty for each element in the prototype chain.)

var context = this._workerContext;
this._workerContext = null;
if (typeof callback === 'function') {
this.on('exit', function(exitCode) {

This comment has been minimized.

@bnoordhuis

bnoordhuis Mar 16, 2015

Member

Maybe this.once() so the closure doesn't stay around?

for (var key in er)
additionalProperties[key] = er[key];
// The main message delivery must always succeed.
JSONStringify(additionalProperties);

This comment has been minimized.

@bnoordhuis

bnoordhuis Mar 16, 2015

Member

Maybe move the for/in out of the try/catch block?

This comment has been minimized.

@petkaantonov

petkaantonov Mar 16, 2015

Contributor

But then this function could throw

This comment has been minimized.

@bnoordhuis

bnoordhuis Apr 7, 2015

Member

What's so bad about throwing? Code that silently swallows errors makes debugging very hard.

This comment has been minimized.

@petkaantonov

petkaantonov Apr 7, 2015

Contributor

This is called from fatalError handler, if it throws, then the real cause for the fatal error will not be discovered

This comment has been minimized.

@bnoordhuis

bnoordhuis Apr 7, 2015

Member

Okay, makes sense. Can you update the comment to explain why it must succeed?

};

Object.defineProperty(Worker, '_workerFatalError', {
configurable: true,

This comment has been minimized.

@bnoordhuis

bnoordhuis Mar 16, 2015

Member

Why configurable? It doesn't seem to be modified anywhere.

This comment has been minimized.

@petkaantonov

petkaantonov Mar 16, 2015

Contributor

My rationale is that read-only should be enough to disable casual or accidental overwrites but configurable is provided as an escape hatch if someone has some really bizarre reason to change it.

@petkaantonov

This comment has been minimized.

Copy link
Contributor

petkaantonov commented Mar 16, 2015

@bnoordhuis The check should be much cheaper than emulation, I mean at the cost of calling HasRealIndexedProperty alone you could have already retrieved a real own indexed property. And only if it's undefined would you need to check if it's a hole, which is a rare case.

to_remove = hc;
break;
}
}

This comment has been minimized.

@bnoordhuis

bnoordhuis Mar 16, 2015

Member

An O(n) operation per closed handle is kind of unfortunate...

This comment has been minimized.

@petkaantonov

petkaantonov Mar 16, 2015

Contributor

Yes I thought about that and we should consider changing this to a structure that allows faster removal

This comment has been minimized.

@petkaantonov

petkaantonov Mar 16, 2015

Contributor

Or if the HandleCleanup object is available to the registerer, removal is constant time


inline void Environment::Exit(int exit_code) {
if (is_main_instance())
exit(exit_code);

This comment has been minimized.

@bnoordhuis

bnoordhuis Mar 16, 2015

Member

Thinking out aloud: that exit() call should probably not be hidden here.

src/env.h Outdated
inline bool CanCallIntoJs() const;
inline void AddSubWorkerContext(WorkerContext* context);
inline void RemoveSubWorkerContext(WorkerContext* context);
WorkerContextList* sub_worker_contexts() {return &sub_worker_contexts_;}

This comment has been minimized.

@bnoordhuis

bnoordhuis Mar 16, 2015

Member

Style: space after {, before }.

For consistency, it'd be better to move it into env-inl.h.

src/env.h Outdated
WorkerContextList sub_worker_contexts_;
int sub_worker_context_count_ = 0;
NodeInstanceType instance_type_ = NodeInstanceType::MAIN;
int handle_cleanup_waiting_ = 0;

This comment has been minimized.

@bnoordhuis

bnoordhuis Mar 16, 2015

Member

Can you use size_t instead of int while you're here?

}

void
HandleWrap::RegisterHandleCleanup(const FunctionCallbackInfo<Value>& args) {

This comment has been minimized.

@bnoordhuis

bnoordhuis Mar 16, 2015

Member

Femto style nit: it's more idiomatic to write that as:

void HandleWrap::RegisterHandleCleanup(
    const FunctionCallbackInfo<Value>& args) {
src/node.cc Outdated

if (args.Length() != 1 || !args[0]->IsString()) {
return env->ThrowTypeError("Bad argument.");
}

node::Utf8Value path(args.GetIsolate(), args[0]);
ScopedLock::Write lock(&process_rwlock);

This comment has been minimized.

@bnoordhuis

bnoordhuis Mar 16, 2015

Member

Just wondering, is it necessary to take out the r/w lock when only the main isolate can call process.chdir()?

EDIT: Let me clarify, is it because you fear uv_cwd() is not thread-safe? It is - or should be.

src/node.cc Outdated
@@ -1597,6 +1618,7 @@ static void Cwd(const FunctionCallbackInfo<Value>& args) {

static void Umask(const FunctionCallbackInfo<Value>& args) {
Environment* env = Environment::GetCurrent(args);
CHECK(env->is_main_instance());

This comment has been minimized.

@bnoordhuis

bnoordhuis Mar 16, 2015

Member

I think it should be safe to call process.umask() from a worker, just not process.umask(mask) (although you could argue it's not safe for the main isolate to change the umask under a worker's feet either.)

src/node.cc Outdated
@@ -1735,18 +1757,21 @@ static gid_t gid_by_name(Isolate* isolate, Handle<Value> value) {

static void GetUid(const FunctionCallbackInfo<Value>& args) {
// uid_t is an uint32_t on all supported platforms.
ScopedLock::Read lock(&process_rwlock);

This comment has been minimized.

@bnoordhuis

bnoordhuis Mar 16, 2015

Member

Doesn't need a lock, thread safety is guaranteed by libuv and the operating system.

src/node.cc Outdated
args.GetReturnValue().Set(static_cast<uint32_t>(getuid()));
}


static void GetGid(const FunctionCallbackInfo<Value>& args) {
// gid_t is an uint32_t on all supported platforms.
ScopedLock::Read lock(&process_rwlock);

This comment has been minimized.

@bnoordhuis
src/node.cc Outdated
@@ -1758,7 +1783,9 @@ static void SetGid(const FunctionCallbackInfo<Value>& args) {
return env->ThrowError("setgid group id does not exist");
}

if (setgid(gid)) {
ScopedLock::Write lock(&process_rwlock);

This comment has been minimized.

@bnoordhuis
src/node.cc Outdated
@@ -1777,7 +1805,9 @@ static void SetUid(const FunctionCallbackInfo<Value>& args) {
return env->ThrowError("setuid user id does not exist");
}

if (setuid(uid)) {
ScopedLock::Write lock(&process_rwlock);
int err = setuid(uid);

This comment has been minimized.

@bnoordhuis

bnoordhuis Mar 16, 2015

Member

And ditto. :-)

src/node.cc Outdated
ngroups = getgroups(ngroups, groups);
{
ScopedLock::Read lock(&process_rwlock);
ngroups = getgroups(ngroups, groups);

This comment has been minimized.

@bnoordhuis
src/node.cc Outdated
gid_t egid;
{
ScopedLock::Read lock(&process_rwlock);
egid = getegid();

This comment has been minimized.

@bnoordhuis
src/node.cc Outdated
@@ -1843,6 +1881,7 @@ static void SetGroups(const FunctionCallbackInfo<Value>& args) {
groups[i] = gid;
}

ScopedLock::Write lock(&process_rwlock);

This comment has been minimized.

@bnoordhuis
src/node.cc Outdated
@@ -1888,6 +1928,7 @@ static void InitGroups(const FunctionCallbackInfo<Value>& args) {
return env->ThrowError("initgroups extra group not found");
}

ScopedLock::Write lock(&process_rwlock);

This comment has been minimized.

@bnoordhuis
src/node.cc Outdated
@@ -1903,7 +1944,7 @@ static void InitGroups(const FunctionCallbackInfo<Value>& args) {


void Exit(const FunctionCallbackInfo<Value>& args) {
exit(args[0]->Int32Value());
Environment::GetCurrent(args)->Exit(args[0]->Int32Value());

This comment has been minimized.

@bnoordhuis

bnoordhuis Mar 16, 2015

Member

I think it would help code clarity to keep the exit() call for the main isolate here.

EDIT: Belay that, I see that it would make the Exit() calls elsewhere much more complicated.

src/node.cc Outdated
int err;
{
ScopedLock::Mutex lock(&process_mutex);
err = uv_resident_set_memory(&rss);

This comment has been minimized.

@bnoordhuis

bnoordhuis Mar 16, 2015

Member

I think this should be MT-safe on all platforms. I would suggest dropping the lock and forwarding all bug reports to libuv/libuv. :-)

src/node.cc Outdated
uint64_t t;
{
ScopedLock::Mutex lock(&process_mutex);
t = uv_hrtime();

This comment has been minimized.

@bnoordhuis

bnoordhuis Mar 16, 2015

Member

You don't have to take out the lock here, uv_hrtime() is MT-safe.

@rvagg

This comment has been minimized.

Copy link
Member

rvagg commented on 2faae58 Jul 4, 2015

FYI I accidentally said "v2.3.2" in the tag message but the v2.3.3 tag is correct

not-implemented and others added some commits Apr 13, 2015

test: add test for missing `close`/`finish` event
See next commit for the actual fix.

PR-URL: #1373
Reviewed-By: Fedor Indutny <fedor@indutny.com>
doc: update AUTHORS list
Update AUTHORS list using tools/update-authors.sh

PR-URL: #2100
Reviewed-By: Ben Noordhuis <info@bnoordhuis.nl>
Reviewed-By: James Hartig <fastest963@gmail.com>
path: refactor for performance and consistency
Improve performance by:
+ Not leaking the `arguments` object!
+ Getting the last character of a string by index, instead of
  with `.substr()` or `.slice()`

Improve code consistency by:
+ Using `[]` instead of `.charAt()` where possible
+ Using a function declaration instead of a var declaration
+ Using `.slice()` with clearer arguments
+ Checking if `dir` is truthy in `win32.format`
  (added tests for this)

Improve both by:
+ Making the reusable `trimArray()` function
+ Standardizing getting certain path statistics with
  the new `win32StatPath()` function

PR-URL: #1778
Reviewed-By: Сковорода Никита Андреевич <chalkerx@gmail.com>
Reviewed-By: Roman Reiss <me@silverwind.io>
benchmark: Add some path benchmarks for #1778
Path functions being benchmarked are:
* format
* isAbsolute
* join
* normalize
* relative
* resolve

PR-URL: #1778
Reviewed-By: Сковорода Никита Андреевич <chalkerx@gmail.com>
Reviewed-By: Roman Reiss <me@silverwind.io>
@mscdex

This comment has been minimized.

Copy link
Contributor

mscdex commented on fb05c8e Jul 4, 2015

Why was it reverted?

This comment has been minimized.

Copy link
Member

indutny replied Jul 4, 2015

It was supposed to go to the next branch.

This comment has been minimized.

Copy link
Contributor

thefourtheye replied Jul 4, 2015

@mscdex It was supposed to be landed in next #1411 (comment)

EDIT: Oops, already clarified by Darkseid :D

zkat and others added some commits Jul 6, 2015

deps: upgrade to npm 2.12.1
PR-URL: #2112
Reviewed-By: Jeremiah Senkpiel <fishrock123@rocketmail.com>
deps: make node-gyp work with io.js
Every npm version bump requires a few patches to be floated on
node-gyp for io.js compatibility. These patches are found in
03d1992,
5de334c, and
da730c7. This commit squashes
them into a single commit.

PR-URL: #990
Reviewed-By: Ben Noordhuis <info@bnoordhuis.nl>
win,node-gyp: enable delay-load hook by default
The delay-load hook allows node.exe/iojs.exe to be renamed. See efadffe
for more background.

PR-URL: #1433
Reviewed-By: Ben Noordhuis <info@bnoordhuis.nl>
doc: document current release procedure
PR-URL: #2099
Author: Jeremiah Senkpiel <fishrock123@rocketmail.com>
test: refactor test-repl-tab-complete
The original test uses a variable to explicitly count how many
times the callback is invoked. This patch uses common.mustCall()
to track if the callback is called or not. This makes the test
more robust, as we don't explicitly hardcode the number of times
to be called.

PR-URL: #2122
Reviewed-By: Colin Ihrig <cjihrig@gmail.com>
tools: install gdbinit from v8 to $PREFIX/share
gdbinit provided by V8 can be very useful for low-level debugging of
crashes in node and in binary addons. Most useful commands at 'jst'
for JS stack traces and 'job' for printing a heap object.

This patch installs the file at $PREFIX/share/doc/node/gdbinit.

Reviewed-By: Ben Noordhuis <info@bnoordhuis.nl>
PR-URL: #2123
test: add missing crypto checks
Add a check for crypto before using it, similar to how
other tests work.

PR-URL: #2129
Reviewed-By: Shigeki Ohtsu <ohtsu@iij.ad.jp>
Reviewed-By: Jeremiah Senkpiel <fishrock123@rocketmail.com>
@alubbe

This comment has been minimized.

Copy link

alubbe commented Jul 8, 2015

Can we do anything to help out @petkaantonov? It would be awesome to play around with this!

@bnoordhuis

This comment has been minimized.

Copy link
Member

bnoordhuis commented Jul 8, 2015

@alubbe There are still some outstanding issues / comments. I don't think anyone will object if you adopt this PR and make the necessary changes. Just fork Petka's branch and add your changes on top.

@petkaantonov petkaantonov force-pushed the petkaantonov:workers-implementation branch from fab6db9 to 5c4dbba Jul 8, 2015

@petkaantonov petkaantonov referenced this pull request Jul 8, 2015

Closed

workers: initial implementation #2133

4 of 4 tasks complete
@petkaantonov

This comment has been minimized.

Copy link
Contributor

petkaantonov commented Jul 8, 2015

#2133 replaces this PR

@formula1 formula1 referenced this pull request Jul 11, 2015

Open

Threads #75

@GnorTech

This comment has been minimized.

Copy link
Member

GnorTech commented Dec 13, 2016

For those who want to write Node.js code in multithread program: NW.js implemented this by enabling Node.js in Web Workers: https://nwjs.io/blog/v0.18.4/

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment