If Unix weren't so committed to doing I/O the stupid way around, maybe Node wouldn't have to provide fragile, ersatz asynchronicity the way that it does?
If Unix weren't so committed to doing I/O the stupid way around, maybe Node wouldn't have to provide fragile, ersatz asynchronicity the way that it does?
* All data has a type
* All I/O is asynchronous and interrupt (event) driven
At its inception Unix doubled down on stupid, first by encouraging data to be stored and migrated in the form of untyped text streams, which require ad-hoc parsing and unparsing logic at the endpoints; and secondly by failing to provide asynchronous I/O primitives to user space. POSIX later provided an AIO standard, but it was cumbersome to use, bolted on rather than integrated into the OS, and virtually nobody used it. What you want to be able to do is submit I/O requests to the kernel, and have the kernel notify you when they are completed while you wait or do other processing; you could even put the process to sleep until pending I/O is completed and have it take up no CPU during this time. Instead what people do is burn CPU cycles in select/poll loops, which are the I/O equivalent of asking "Are we there yet? Are we there yet? Are we there yet?" over and over. You can fake asynchronicity by doing a bit of processing before asking "are we there yet?" again, but you have to write your processing code in such a manner as to be done piecewise in a loop, and the lower latency you want the more frequently you have to poll (and the more CPU you have to burn polling). This is how Node does "asynchronous" I/O; the VM stops every few instructions to poll for ready FDs, dispatches to callbacks as necessary, and performs parts of pending large I/O operations which can be done non-blocking. (That's the other sucky bit: you can't just call read(2) and write(2) for large chunks of data on an fd that's been opened O_NONBLOCK and expect it all to work; you have to keep reading or writing in a loop when the fd is ready, subtracting the number of bytes successfully written until the entire buffer is processed.)
But if something -- a call into a C library for instance -- stops the VM from doing the poll and I/O bits of its main loop, all I/O simply... stops. And your throughput goes into the toilet. Whereas if the runtime had been based on an OS that natively supports interrupt-driven AIO -- like VMS, Windows, or AmigaOS -- it would be extremely difficult to stall the I/O pipeline completely this way. You might be able to stall further I/O calls with a really long operation, but any pending calls already initiated would complete, and the runtime would be properly notified of their completion.
And all this stems from the fact that Unix was the Node.js of its day -- an environment designed to make things easy for casual programmers, not to do things correctly.
I tend to agree with you here: it'd have been better had Unix offered a typed abstraction over byte streams, but OTOH its approach really was a Worse is Better deal. Building a large set of tools which can deal with typed streams (or even trees, or graphs …) takes a long time, and is error-prone: building a few simple tools which treat everything as text is quick & easy. Of course, it does mean that the next four and a half decades are spent dealing with a lack of types, but it means one can ship quickly.
> Instead what people do is burn CPU cycles in select/poll loops
I thought that select/poll blocked, but it's been a long time since I did anything low-level like that, so I could be wrong.
> And all this stems from the fact that Unix was the Node.js of its day -- an environment designed to make things easy for casual programmers, not to do things correctly.
Well, Worse is Better.
Well, yes. That's just the problem. ALL I/O ops in Unix block. Even the ones on O_NONBLOCK fds, which simply read or write -- blockingly -- until the underlying kernel buffer is full/empty, then return.
You don't have the option to request I/O, do some processing, and then wait until the I/O completes. If you want to interleave processing with I/O you have to buzz in a select loop, doing your I/O and processing in tiny explicit chunks. Or spawn subprocesses to do I/O, leadibg to more complexity and confusion.
The facilities in the Windows kernel for I/O are strictly more robust and flexible than what Unix (eve Linux and BSD with kqueue/epoll) provides. The designer of the Windows kernel, Dave Cutler, also designed VMS, knew what real-world I/O requirements were, and famously mocked the way Unix handles I/O.
That's what select() does though: you give it a timeout, and it goes to sleep until either an fd becomes ready or the timeout expires. As an example, the X server on you typical linux laptop uses select() on a whole bunch of sockets, one for each client, and it both has good enough latency to be interactive, and doesn't burn a lot of CPU doing so. Is this the most convenient programming model for this? Probably not. And of course it still has the issue that a long-running function can delay your getting back into the select loop.
Select is still not nearly as flexible as Windows I/O completion ports, for instance. You cannot kick off an I/O operation, do some other processing, and then wait for the I/O to complete before you use its results with a select loop. You have to do a little bit of processing, pump and poll, do a little bit more processing, pump and poll, etc. You are wasting cpu time compared to the interrupt-driven way, and if you have many processes going at once this can add up in terms of battery power used and latency suffered. Remember that select(2) and poll(2) are system calls -- and all the overhead that goes with that.
The X server, in its effort to implement a GUI which is by nature event driven, historically contained all sorts of hacks in order to simulate the asynchronicity which should come from the OS. And it has always been slow and laggy compared to its counterparts on Windows and Mac OS -- let alone the Amiga where interrupt-driven programming is the norm and the GUI has always been flawlessly responsive even when other tasks are going on.
There's also the fact that nonblocking I/O IS NOT ASYNCHRONOUS I/O. If the kernel needs time to perform an I/O operation -- for example, reads and writes from/to disk -- it WILL block your process doing so. All "nonblocking" really means is "read until read buffer is drained, write until write buffer is full, then return". Your process is still stopped during the read/write! With better primitives such as Windows I/O completion ports that map more closely to the asynchronous I/O subsystems of the underlying architecture -- DMA, interrupts, etc. -- that becomes a non-issue. I/O happens transparently in background from an application (and maybe even kernel) perspective, and nothing is blocked.
In short, nearly anything at all is better than the Unix model in terms of throughput, latency, and more natural programming style. Unix is actively regressive.
One feature that was implemented in the Amiga OS, BeOS, and just about nowhere else is the concept of datatypes. Third-party software could register OS-wide readers and writers for their file formats, allowing files of that type to be consumed or produced by any application.
PowerShell lets you construct pipelines of typed objects with fields and methods, not just untyped dumb text.