> Then I would ask, where does it end?

Slippery slope fallacy.

> If you have it spin off a separate async task and yield to the original one, that's still filling up your thread pool.

No it's not because the original task yielded so it's just sitting there waiting not taking up resources other than memory. It's not "filling up your thread pool".

> This too is a little contrived, but I don't see a hard distinction between this and the IO case

This is correct. A long running function might as well be blocking. The difference, generally, is that a long running function is presumably doing work whilst a function that blocks is waiting consuming compute resources literally doing nothing. The first case is generally considered acceptable because the function needs the processor/resources. The 2nd case is not because it's a waste. Surely you can see that distinction.

In fact, this is the whole reason async/await exists. It's first class syntax to express a scenario that shows up commonly in application machinery: thread pool executing tasks to avoid the massive numbers of blocked threads which traditionally happens in a thread model. And you should know that by definition async/await is not a good fit for tasks that take a long time to compute exactly for this reason we've been discussing. So much so that async runtimes provide an escape hatch for when your async task needs to do something that blocks the task execution threads: spawn_blocking[1].

As a programmer using async/await, you're supposed to know this to write correct async programs... just like you were supposed to know how to manage your pointers. Hardly any, especially those coming from JS, do. So the result is super sloppy and not very performant async code. And sadly people think it's the runtime's fault for not being good at scheduling or just not being performant or something. It's hilariously sad.

1: https://docs.rs/tokio/latest/tokio/task/fn.spawn_blocking.ht..., https://docs.rs/async-std/1.2.0/async_std/task/fn.spawn_bloc...

> At some point: the computer has finite resources, and those can get overloaded.

Yes, as is the case for any problem of computer engineering, one doesn't run code on an infinitely perfect machine. This isn't news.

> There's something here around how async runtimes might not be making full use of the machine, which is an interesting thread (no pun intended) to pull on.

If you didn't before when claiming that yielded tasks fill up a threadpool, here is where you betray your lack of experience on this topic. async/await (and generally the task/executor paradigm) is actually a solution put forth to maximize the use of available compute resources. That doesn't mean every runtime, or more likely the programer's application code, is good at doing that, but that's besides the point. The problem that tons of threads causes is that historically machine threads (as opposed to green threads like Go) require context switching. Context switching is expensive so if you let tons of them pile up you eventually spend more time context switching than actually computing, thus waste resources. In reality this is only a problem for incredibly highly parallel high throughput systems. Sometimes this means a web server.

> But I don't think we can expect the language (at least as currently designed) to fully prevent this class of problem.

Of course not. The language doesn't fully prevent all instances of memory un-safety either, but it gets pretty darn far.

> And I also don't think the standard library should orient itself around async use-cases, which are still a minority of Rust uses, especially for the sake of an edge-case like this one

Nobody is asking this. It's just not how this does or would work.

You're taking a pretty weak position. Just because we can't make something perfect in all cases is not justification for not improving it in most cases. If it were, we'd not have put any effort towards making a memory safe language in the first place and Rust wouldn't exist (contrary to what you might think, Rust programs don't prevent all memory safety issues). It is entirely possible to inadvertently stall your async program under normal usage and you don't need to delve into endlessly computing tasks to do so. As I've explained this can happen almost comically easily as soon as you introduce any type of waiting e.g. semaphores, sockets that a client keeps open, etc. which are all vastly more common than endless computation and entirely normal.

There are two classes of problems here: (1) programmer errors by using the incorrect blocking version of a call, and (2) trying to run super compute heavy workloads. I'm arguing that steps be taken to vastly reduce the possibility of (1) despite the fact that (2) can still happen. Who cares about (2) if we all but eliminate (1)? (Or, once we do we can take a shot at addressing (2).) And the solution is not black magic or something and it actually exists in some other async/await languages. This isn't fiction. It's 100% possible to mark blocking calls and have the complier tell async callers that this function needs special care, such as use of spawn_blocking, if you want to call it from async code. Let the caller disable it with an attribute if they want to shoot themselves in the foot who cares. Just make it better in the general case of "users don't know that they shouldn't call this blocking thing in an async task". For "normal" Rust usage absolutely nothing changes at all.

But even if it cost semantics, Rust's entire shtick is that it helps users write correct programs at the cost of more annoying semantics. I'm not arguing for polluting the stdlib to "orient it around async use cases". I'm arguing to add semantics so that it works normally in normal use cases and prevents unsuspecting users from shooting themselves in the foot when used in async use cases. async/await is not some "minority use case". It's been in Rust for over 4 years now and is remarkably common in my day to day usage of Rust.