How does event-driven programming even work?
thesystemsprogrammer.com
thesystemsprogrammer.com
Different runtimes can have different threading models, it appears Node.js is running their loops in a cooperative threading model, so they are not spawning "true" threads but pseudo threads, which all run off 1 thread.
It also looks like node.js is a bit like Java in that it has a multi platform backend to run code on a variety of platforms.
Windows as an OS allows both threading models, and the explanations seen in windows literature for event driven processing can be applied with albeit sometimes different terminology to other languages and runtimes.
https://learn.microsoft.com/en-us/windows/win32/winmsg/using...
True threads also allow a few more options, like thread priority and thus more cpu time to complete a task, unlike the 1 thread cooperative threading model, which all run at the fixed rate of the 1 thread.
Because of this, true threads can mask the time to process a large file, if it can detect the file size and automatically give the thread a higher cpu priority to reduce the job time when the thread is started. True threads with faster cpu priority are an attack vector, because a true thread with maximum cpu priority coule read and make changes to a file as its being written out by a thread running at normal priority especially if read write by others is allowed and there is no transactional file locking in operation.
Think of it like a blind brick layer working at a fixed speed, and another faster person is able to change the bricks once they have been laid, or before being picked up by the blind brick layer.
Performance of threads can be controlled by the OS when looking at cpu scheduling a system wide setting which requires reboots. https://en.wikipedia.org/wiki/Scheduling_(computing)#Schedul...
As an aside, JS also support true threads in the form of Worker but does so in a way that prevents deadlocks while also not using explicit locking mechanisms. You are correct that there are no low-level facilities for thread (de)prioritization. Then again, if you needed that low-level fine-grained control, you'd either not be using JS or you'd write a module in a language that does for that one narrow instance where profiling has determined it will help.
A large majority of the time in any language, the naive, readable approach is sufficient. Only after profiling exactly where the hotspots are should you go spelunking.
But it also hides an attack vector I've yet to find mentioned anywhere, in much the same way multi cores can also help hide data manipulation, compared to a single core.
Isolates can transfer ownership of data, but the current primitives are just godawful. It’s like someone built a foot trebuchet out guns. I tend to think of myself as pretty damned sharp, especially about concurrency, and I would not trust myself to get that code right. I can’t think of anyone I would, and I used to know some people on the Java Concurrency committee. It’s a non-feature.
So what most people do is messaging, which is pretty un-exotic RPC.
I see no inherent fighting over shared state in Rust yet it clearly supports threads. "True threads" don't "fight" over shared state; they transfer ownership, whether that be through a borrow checker or with a mutex. The "fight" generally occurs when you get ownership wrong, such as in C++ or Java.
It's a distinction without a difference to identify them as isolates. Do you have as much flexibility with JS as with other languages that support parallelism? Of course not; that wasn't the design goal and usability tradeoff for JS. However, transferring ownership of shared state between two or more actively running execution environments within the same process (which is what Workers do in JS) is what most folks would describe as threads. Isolates are in fact a subset of… threads.
All isolates are threads but not all threads are isolates.
> It's a distinction without a difference to identify them as isolates.
What are you talking about? They have a distinct address space per thread. The threads can only talk to each other via messages. Which require JSON parse and stringify rounds (meaning you can’t avoid event stalls on large JSON messages) They cannot share state. Those are not threads. They’re isolates.
[Edit: autocomplete gibberish]
https://developer.chrome.com/blog/transferable-objects-light...
But this is in the context of todays "modern operating systems", other operating systems, past and present might have different terminology and ways of working, just like the terminology used in frameworks/runtimes can differ.
Half the problem with any domain of knowledge, is trying to acquire accurate knowledge in the first place.
1. The state of the application is at rest.
2. An event happens (a message is received, a timeout, a key press, …) and the state of the application is updated to a new state.
3. Goto 1
In other words, think about the application as a large state machine with each event being a transition from one state to another. It’s that simple really.
How the events are detected and processed is system/OS/API/… dependent. It can be done in many ways. But the basic idea is the same.
At one point in my career I’d seen and heard this failure mode so often I swore never to use one again. I don’t mean never use events, that would not be “crazy like a fox” but just regular crazy. I mean not as the organizing principle. When event dispatch is small, isolated islands of state, then the isolation prevents this from happening.
When it’s central and everyone is convinced to use it, then errors accumulate. No one can watch all parts of the system for nasty bugs. We pick our battles. But when all parts of the system (can) touch the same thing, then you have to assume everything touches everything else.
If you take the right snippets out of what I just wrote, you might think I was talking about global shared state. Which it pretty much is.
Using parallelism/concurrency is fine. As long as the data dependencies doesn’t have the usual multi-threading bugs (a thread modifying data that another thread is reading etc.)
What's immensely mentally blocking me is how such high-level, admittedly elegant solutions translate down. Async code writes naturally in JS/Node.js, great; but it's imperative code on the lower levels, assembly even. The article mainly defers to libuv. But how does it work?
Add to that the different concurrency models. Some runtimes are async and multithreaded (.NET, don't know about JVM, ...), some are single-threaded async (Python, I guess Node.js as well, ...). Some are native code async, which can be either single- or multithreaded (Rust).
I'm aware of coroutines and how they do cooperative multitasking, green threads, native threads, ...
A large, complex landscape that I have been trying to grok and fit into a unified mental model, but I am slowly losing hope that's even possible. Things seem too heterogeneous.
If anyone knows a good resource (preferably book) on this topic, I'd love to hear about it.
There clearly is something doing the scheduling and dispatching somewhere in the stack. There will most likely be queues involved and data structures linking receivers and emitters if you are using events. If you use system threads, they might actually be multiple schedulers involved (the OS one and the runtime one).
My advice is to pick up a system book about designing OS which will have a section about scheduling and work from there. Runtime schedulers are not that different.
if the os doesn’t do this, the code running on the os can.
then there’s interrupts. if there is no threading loop, you can still register interrupts. this is a fundamental concept in cpu design. every cpu has some way of interrupting the current task and run some function. so you can register an interrupt handler for certain types of interrupts, and that code then is run instead of whatever code is running then when the interrupt is handled it does back to the place it was. It looks like you have non imperative code because 2 things happened close enough to look simultaneous, but its still sliced.