Please, explain what do you mean by that - "pre-emptive"?
My understanding (coming from C and OS terms) is that pre-emptive means taken over. e.g. if I have a real OS thread it is being temporarily "paused" and the resources (CPU/mem/io) are given to something else. At some point control is restored back.
But this is without the knowledge or instructions from the thread itself. So things like priority inversions are hard to battle with pre-emptive multitasking - for example thread A with low priority holding a mutex, while thread B with higher priority waits for it. (and no need for mutexes, if only message passing is to be used).
The node.js worker threads are native threads, which are preemptive on all current platforms. The JavaScript context is running an event loop which most likely must perform locking on its message queue and callbacks to async operations are queued for execution on a future tick of this event loop. All of this seems very preemptive to me.
What seems like cooperation in node.js is really just async operations queuing up on the event loop. Since requests are also async events, they get interlocked with callbacks from existing requests.
To me, cooperation is when you yield the thread to another coroutine. This saves the state of the call stack, the registers, everything; meaning you don't force your user to keep that state in closures. The user code in a cooperative environment feels sequential and blocking and results are passed by return values, not by calling continuations.
Its also friendlier to exceptions since it doesn't lose the entire call stack; with node.js you only get the stack since the beginning of the current event loop's tick.
> The JavaScript context is running an event loop which most likely must perform locking on its message queue and callbacks to async operations are queued for execution on a future tick of this event loop. All of this seems very preemptive to me.
There’s a single loop which blocks until the task yields while waiting on the result from one of the worker threads. That’s cooperation. All queued connections are starved until that happens. In Erlang, or just with pthreads, the connections are processed independently. Think separate event loops for each connection.
> To me, cooperation is when you yield the thread to another coroutine. This saves the state of the call stack, the registers, everything; meaning you don't force your user to keep that state in closures. The user code in a cooperative environment feels sequential and blocking and results are passed by return values, not by calling continuations.
That has noting to do with how execution is scheduled. Cooperative scheduling requires passing continuations[1], so the execution can be resumed while it waits. The simplest implementation is to use callbacks, the way node.js does it. Futures and deferreds[2] are a little bit more sophisticated (Python’s Twisted, probably something for node.js exists as well), as they allow for better composition. And of course you can hide the continuations entirely, which can be done in both Scala (compiler plugin) and Python (gevent or using generators), rewriting the direct control flow by breaking it on yield points automatically (this is how exception throws work in most languages btw), but the limitations inherent in having a single event loop per thread will still exist.
> a single loop which blocks until the task yields while waiting on the result from one of the worker threads
Yes, node.js is cooperative, yet since all I/O is asynchronous the time spent blocking is mostly dispatching and simple operations, it doesn't block while waiting - that's where it's performance and high concurrency comes from. Doing CPU-heavy work in the server/main process is a no-no.
Obviously that approach is fine enough for many things. Before node.js, people have written those kinds of servers in Twisted or Netty, with great results. Netty based framework powers, for example, much of Twitter. I was just explaining how the scheduling works :)
My understanding (coming from C and OS terms) is that pre-emptive means taken over. e.g. if I have a real OS thread it is being temporarily "paused" and the resources (CPU/mem/io) are given to something else. At some point control is restored back.
But this is without the knowledge or instructions from the thread itself. So things like priority inversions are hard to battle with pre-emptive multitasking - for example thread A with low priority holding a mutex, while thread B with higher priority waits for it. (and no need for mutexes, if only message passing is to be used).