Remix.run Logo
jmyeet 5 hours ago

I think it's important to understand how we got here and a lot of it has to do with serving network requests or RPCs.

The first Web servers used CGI (Common Gateway Interface). This spawned an entire program (process) per request and had obvious overheads. This led to some optimizations (eg FastCGI, ISAPI/NSAPI) to reduce the overhead. This was the era of Perl scripts being popular.

Then came the model of having a persistent state across requests. Java servlets were a big example of this. Given the cost of exec'ing a process, this was a big deal. But then you've immediately created an environment where multiple threads were accessing the same resource and you could leak resources. There were other variants like CORBA.

Now this was abotu the time of the birth of PHP. PHP was revolutionary because it had a stateless core that allowed shared hosting environments, which were exceptionally cheap for the time (even though they had security issues). But the idea is that you avoided the threading issues of a persistent environment and didn't really have resource leaks because everything got torn down. Of course PHP had other issues. But this was a big deal because things like the initialization of a JVM class loader, for example, was relatively expensive and you had to tune Java servers around performance and STW GC pauses.

None of the above really has anything to do with programming languages other than people learned (or didn't learn) just how hard writing multithreaded code is, something that is true to this day and you absoultely want to avoid it if possible. It is incredibly difficult to get right in an era of cores, threads, different L1/L2 caches, out-of-order proccessing, branch prediction, etc etc etc. And your code may have to run on multiple architectures.

Now Go chose to get around this issue with goroutines and channels. I personally think these are a bad abstraction, particularly because buffered channels are used without understanding the impact (leading to deadlocks), you can have exploding goroutine sttacks and using unbuffered channels is a strictly inferior (IMHO) async/await abstraction.

I actually think that Facebook's Hack has basically the almost perfect async/await. The whole idea of async/await is that you get the benefits of the PHP model of being single-threaded in your own application code and you can tear down your environment when you're done. Any IO goes through async API functions.

Now how does the scheduler manage threads, exhaustion, etc in this environment? Honestly? I have no idea. It just basically works. So maybe the Tokio issue is that the scheduler itself is blocked, which seems to be the case from this article. That does seem like a flaw but a fixable one.

I get the whole colored function criticism but the reality of using Hack to serve HTTP requests is that everything is async anyway so it seems to be a non-issue in practice. You can if you really need to call call an async function from a non-async function with a blocking function but that's not best practice.

I do know that thread pools, particularly multiple thread pools, is not the answer.

lioeters 5 hours ago | parent | next [-]

That's an informative overview. Somewhere in the story was Node.js and libuv, the callback style, promises, and the popularization of the async/await paradigm. Not sure if there was a direct influence on Rust's async libraries, but I imagine it affected how some people think what an intuitive async syntax might look like.

oaiey 4 hours ago | parent [-]

Particularly node.js single threaded operational model basically eradicating concurrency from the brains of a generation of developers.

WatchDog 5 hours ago | parent | prev [-]

Your history is all valid, but I don't think it really hits on the main motivations for how we got here.

Thread per request works perfectly fine if your application is CPU constrained.

However the observation was made, that most web applications are IO constrained, the majority of the time spent serving a web request is spent waiting for a database or downstream API.

Since most of the threads are idle waiting, your application needs many threads to optimally utilize the servers resources.

There was a perception(valid or not) that OS threads have too much memory and scheduling overhead.

Nginx came out using async io, and it could handle much more concurrent requests than apache, which used a threaded model, it sparked a lot of interest in different kinds of application managed scheduling.

It inspired initiatives like the reactive manifesto[0], which spawned tools like RXJava.

[0]: https://reactivemanifesto.org/