3 ms·
> the state machines are handled by the compiler I think this is the aspect of modern approaches async which I am most ambivalent about. One of the things I h
by skohan 6y ago
> the state machines are handled by the compiler
I think this is the aspect of modern approaches async which I am most ambivalent about. One of the things I have learned about programming over the past 10 years is that I, as the programmer, really want to own the flow of control of my program. Once I hand that over to some other system, usually in the name of convenience, I will start to have issues which are difficult to understand and solve.
For instance, a while ago, I was working on a project which was using making heavy use of RXJava. One of my colleagues pushed a commit, and suddenly CI was failing on a unit test which passed when run locally. It turns out the problem was because the CI server was running tests with a different scheduler, so GC was happening at a different time, creating an NPE which didn't happen locally. Imo when you start to see unit tests behaving inconsistently based on a factor which is completely outside the actual code you yourself have written, this is a sign you are going down the wrong path.
I also wonder how much a lot of the buzz around async actually has to do with the fact that it's a bit brain-bending to wrap your head around at first, as compared to its actual utility. I think for a lot of us as programmers, we enjoy that feeling of understanding something difficult - like when you really get recursion for the first time - and we're attracted to the idea of really fundamentally new concepts being introduced to programming.
But it seems to me that async is one of those concepts which brings us farther away from actually programming the hardware, and puts a kind of middle-man between us and the CPU, and I'm not sure that has ever been a good thing.
- gr__or 6y agoI think the pain you went through with Java is not quite compareable, as the described havoc (and I do really feel your pain here) would not happen like that in Rust, for the reason that you would have to model these things more explicitly. In detail, it sounds like a cascade of implicit null-ness (aka the billion dollar mistake) and weak references. In Rust null (called None) is explicit through the Option type and the Weak type returns exactly that. More to your point, as I think you were using this as an analogy for the perils of giving up control: Rust's explicitness should entail all the semantics of your program, and hence async Rust makes you model out all the potentially-racy async interactions with Arc, Mutex, etc. The same middle-man (the borrow checker) who watches over your regular ol' sync code's memory-correctness, now expects extra constraints to be upheld for values passing through async-boundaries. And for me the whole point of Rust is that this correctness proof will do a better job than any person could, for any moderately sized program. So this is middle man you'd want between you and the CPU. That said, your async runtime could definitely do shenanigans that screw up your nicely modeled program, but that would be a bug in that specific runtime. I haven't deeply read Tokio's source and even if I did, making a qualified judgment about it is beyond me.
- skohan 6y agoSo I would not say that the borrow checker is a middle-man. The borrow checker does impose constraints on programming, but at the end of the day it's only relevant at compile time, and you still end up with code which maps in a predictable way to the hardware. If you hand me a synchronous rust code, I can imagine, at least in some approximate way, which set of assembly code would be equivalent. An async runtime is a totally different animal. If you hand me a block of async rust code and ask me how it will execute, the answer I have to give is "it depends on the runtime". This is the disconnect I am talking about.
- gr__or 6y agoFair distinction, thanks for making it! My async use cases so far have been in a realm where everything modeled was everything I cared about, and those specifics of the runtime didn't become relevant. I wanted to refer to your example because I do see how that is a thing that needs to be explicitly modeled. I'd be curious to hear about examples where the runtime did or would surprise you!
- skohan 6y agoI don't have a ton of experience writing async rust programs, but I can imagine some types of problems which might come up: - So what if I am implementing a high-throughput, performance critical system which makes heavy use of async, and under certain circumstances the runtime I'm using falls off a performance cliff. It's going to be difficult to diagnose and solve this problem, because the critical path of my program actually winds through a library which is essentially a black box to me. - What if I have two dependencies, and each one internally depends on a separate async runtime. And what if each of these runtimes is designed with the assumption that it is the main owner of system resources, like threads. There may be conflicts which are very difficult to understand but have effects on the performance of my program. I think fundamentally, an issue with this type of "middleware" is that by its nature, an async runtime, like Tokio for example, has to be implemented with a lot of assumptions about how "the generic program" should optimally handle async. It may work great for the vast majority of use-cases, but fundamentally whenever you design a super general, abstract system like this you have to make tradeoffs. In some ways Rust has taken probably the best possible approach to this, by making it modular and allowing you to bring your own runtime, but I think in practice, if the use of async continues to become pervasive in Rust and certain libraries get locked into certain ecosystems, it will not be so easy in practice to take advantage of that modularity.