5 ms·
> In the worst-case though, wouldn't it just be equivalent to a non-async version of the program? No. You can absolutely stall (I'm hesitant to use the phrase
by dcow 4y ago
> In the worst-case though, wouldn't it just be equivalent to a non-async version of the program?
No. You can absolutely stall (I'm hesitant to use the phrase deadlock because you're not locking, specifically) an async program. More specifically, you can block all of the available scheduler threads. To see for yourself, spawn a bunch of async tasks in a loop that all increment and print a shared counter and then sleep forever. The counter will stop incrementing at some point. Compare that number to the number of scheduler threads your runtime uses. It should look similar.
I think maybe you're conceptually comparing a single threaded Rust program to an async program, which isn't quite accurate. A "normal" program in my experience has many threads. All of these threads can block. One of them blocking does not inherently slow down the rest of the program. Try the above example but spawn a new thread instead of a new async task during each loop iteration. The number will increment much higher, at least until you've exhausted some system limit on the number of threads a process can have.
Maybe there are fancy async runtimes that dynamically expand their threadpool as the number of blocked tasks increases. But naively they all use a threadpool sized at some multiple of the number of real cores your system has, and so will eventually stall if starved.
In any case, it's not generally async-safe to block any thread, ever (obviously if you could prove that the number of async tasks that block is always less than the number of threads the runtime uses, this doesn't hold, but that's a rather wild assumption). And it's impossible for say a library to know the context in which it's being used, so it's never correct for a library that is to be called in an async context to block. It must always yield.
You're right that in the common case things will just appear to slow down since most things don't block for a long time and eventually continue. But there are gnarly edge cases where the entire program stalls. Hence my comparison to memory safety. In the common case memory corruption and dangling pointers aren't horrible they just sometimes cause spurious errors which most programs can recover from. But every once in awhile they cause really bad problems which programs can't recover. Hence why we care.
For a real world example, imagine a server that processes requests. In the async handler block, for some requests you access the filesystem and the device starts experiencing degraded performance due to media errors and read calls never complete. Should the server stop processing healthchecks because some of the requests are having trouble? The answer is almost certainly no. Instead the server should report in its healthcheck that one of its block devices has an increased error rate and tasks are piling up.
Another case would be one involving locks. If you are dealing with a program that involves many workers/consumers waiting for a producer to produce a value and the workers use a blocking api to wait on a semaphore before reading a next value from memory and continuing, the program will almost certainly deadlock not because of a logic error leading to an imbalanced semaphore but instead because the async runtime will stall and the producer will never produce a value. If instead the tasks yield as they should, the program would progress normally.
- brundolf 4y ago> I think maybe you're conceptually comparing a single threaded Rust program to an async program Ok yeah, I think this was the disconnect > spawn a bunch of async tasks in a loop that all increment and print a shared counter and then sleep forever My instinct is "duh, of course sleeping/looping forever will prevent progress". I can see how in a multithreaded program it might not (if it's written a certain way), but that feels like the exception, not a norm/intuitive assumption. Of course these things are highly subjective In my view it's an exaggeration to call functions that might block for a long time "not async safe". Especially since it's in no way worse than the single-threaded case (are they also "not sync safe"?). I think for most people the blocking behavior will be obvious/intuitive, even if it has the potential to cause undesirable behavior (just like any program logic could) It may be a matter of changing expectations- async programming is mainly about interlacing tasks on a thread, not multithreading; it's useful even when there's only one single thread available. It just so happens that you can also spread it across multiple threads, but I don't think it should be expected to live up to every expectation we might have around fully, manually multi-threaded logic
- dcow 4y agoI edited my comment and added a few real world scenarios. In short, a "many consumers single producer" program where the consumers use a blocking API to lock on a semaphore/latch gating access to shared memory updated by the producer is bound to stall. The workers will all block waiting for the producer to produce a value but it never will because the executor is starved. This setup is 100% correct in a traditional program where each worker gets a thread. But it's 100% incorrect when using async/await. The async-safe variant of this setup is to use async-aware semaphore/latch APIs which yield instead of block. I've seen this happen in non-contrived scenarios in the real world. It's not common, I'll give you that, but definitely not contrived. Edit: I disagree about expectations. Async programs should absolutely work correctly in all scenarios that aren't programmer/logic errors. In my example(s) the solution is not "rewrite the program the program is wrong". The solution is "use the correct async-aware function at the call site and the issue goes away". The fact that Rust is being used to write web servers means that async/await is up to the task of handling real world highly parallel programs. You just have to be careful to avoid some nuanced traps, which the Rust compiler currently doesn't assist you with. It totally should!