13 ms·
This article was a whole lot of "this pain you feel, cherish it, it's good". It embraces a defect introduced for BC reasons as if it's sound engineering. It re
by slver 6y ago
This article was a whole lot of "this pain you feel, cherish it, it's good".
It embraces a defect introduced for BC reasons as if it's sound engineering. It really isn't.
If you start over, there's no reason any blocking API should really BLOCK your program. Your functions can look completely synchronous, but yield to the event loop when they invoke a blocking API.
Basically like Erlang works.
Eventually it will be the case, but these kind of "love the flaw" attitudes are slowing us down.
- rcxdude 6y agoThis can work, but it's a fundamentally quite different async model and you give up a lot of the benefits because you have stackful coroutines, which are a lot more heavyweight than the state machines which rust's async functions compile down to. There's nothing stopping you implementing this concept in rust today (a la gevent in python), it's just a seperate thing (and you still need to be careful about accidentlly calling something which does block).
- zozbot234 6y ago> Your functions can look completely synchronous, but yield to the event loop when they invoke a blocking API. This require either runtime-based or operating system support. If need be, you can always rely on the OS scheduler to be your "async blocking API".
- jhgb 6y ago> If need be, you can always rely on the OS scheduler to be your "async blocking API". Well, the original red-blue function article did say "just use threads", didn't it?
- slver 6y agoYou need operating system support in the form of async I/O, which every OS offers. No special runtimes, no threads required.
- pas 6y agoThis is becoming more and more true with io_uring, but it was absolutely not the case a few years ago. (Linux AIO was a strange beast. Direct IO was great with preallocated files, but there was no way to do fsync() - especially on file/directory metadata - in an async way. Plus there was a threadpool based glibc thing that somehow faked async I/O, righ?)
- simias 6y ago>If you start over, there's no reason any blocking API should really BLOCK your program. Your functions can look completely synchronous, but yield to the event loop when they invoke a blocking API. That implies a level of sophistication and abstraction that simply doesn't exist today, and probably won't for a while. For this to work you either need an ultra-sophisticated compiler what will transparently auto-async your code at compile time (as realistic as a unicorn at this point) or a heavy runtime which will have performance implications. So in the meanwhile I agree with TFA that coloured functions is probably the pragmatic choice if you want pay-for-what-you-use async right now. Personally my main issue with async is that it seems to me that a generation of devs who grew up on JavaScript see it as the Alpha and Omega, the one and only way of handling any sort of parallel or event-driven system, not considering other approaches such as threads and/or polling. This sometimes results in spaghetti code that I personally find over-engineered. To give you an example I hacked on rbw a week ago, a Rust command line client for Bitwarden. It's a good program, but it's completely built with async code and I genuinely don't get it given that it's almost entirely synchronous in practice (you query an entry, you enter your passphrase, you get a result). As a result I found it a lot harder to follow the flow of the program and understand how it all fit together. But hey, I eventually got it and managed to fix the issue I was debugging, so it's not the end of the world.
- meowface 6y ago>That implies a level of sophistication and abstraction that simply doesn't exist today, and probably won't for a while. >For this to work you either need an ultra-sophisticated compiler what will transparently auto-async your code at compile time (as realistic as a unicorn at this point) or a heavy runtime which will have performance implications. I may be misunderstanding this point, but if I'm not, it seems like gevent managed to do this for Python a decade ago and it still works well today. I still use gevent for new code because it gracefully sidesteps this function-coloring problem. (I know people don't normally associate "monkey-patch" with "graceful", but they somehow made it work. I guess this falls under the "heavy runtime" approach, but it doesn't feel heavy at all.)
- simias 6y ago
- SebastianKra 6y agoThis would mean that there's no guarantee about the order in which your code will run, because your scheduler might decide to execute some unrelated code right in between any two lines. Therefore, you would need to deal with far more race conditions. Even in single-threaded languages, your entire state may change during a single asynchronous function call. By differentiating between synchronous code (i.e. code that needs to be run in a single slot) and asynchronous functions, you can immediately see where you need to account for unexpected state changes. (Yes, it's more complicated in multithreaded languages, but the main idea still holds because they usually all sync back to a single thread)
- mikepurvis 6y agoBut per the original function colouring article (and summarized at the top of this one) several major parallel-first languages already do this, notably Go and Erlang. And it seems to work great for them.
- lmm 6y agoYou can do it, but at a cost. Interop via the system ABI becomes much harder. You lose the ability to write high-performance blocking code, and reasoning about performance requires understanding a lot of details of the runtime. And to get to the grandparent's point, you have to find a way to protect yourself from the problems of shared-memory threading; Erlang does this by adopting a strict ownership model that's not directly tied to threads (actors), which forces you to structure your programs a particular way and makes certain things impossible. Go has a less strictly enforced model and ends up being quite prone to race conditions, but keeps it under control by discouraging writing large programs at all (in favour of the "microservice" style).
- shawnz 6y ago> If you start over, there's no reason any blocking API should really BLOCK your program. Your functions can look completely synchronous, but yield to the event loop when they invoke a blocking API. Like the article points out, then you would need green threads to do asynchronous work simultaneously. And if you are already using threads then there is no need for "async". The point of "async" is to give you the ability to efficiently manage the event loop WITHOUT threads.
- slver 6y agoGreen threads aren't actual threads. They're an abstraction letting you have an async event loop, but writing synchronous-like code. It's precisely what you do need.
- SahAssar 6y agoHow do you feel about the example in the article? Is it unnecessary performance optimization to parallelize multiple adjacent asynchronous functions or should we just hope that the runtime/compiler gets smart enough to know what is safe to parallelize?
- spinningslate 6y agoNot GP but: > Is it unnecessary performance optimization to parallelize multiple adjacent asynchronous functions? The important word in your question is "async". Take that out and the answer is "no" - it's not unnecessary to parallelise. But the example serves well to illustrate at least one aspect of why red/green is wrong in my opinion: who decides what should be able to run concurrently? Let's modify the example slightly, and assume we have a function `size(url) -> Int` that returns the size in bytes of a given URL. It's making network calls, so it'll have latency. Should the function be async? As the developer of that function, I can't reasonably make that choice beyond "probably". But it comes with a cost. The caller can't now just call it: they have to await, and perhaps unbox the answer. If they're only calling it once then you've just added unnecessary complexity to their code. The _callee_ is the one who's appropriately placed to decide. Using the example, we're calling it 3 times. So, as the developer writing the _calling_ function, I can decide that it's appropriate to paralellise. To use GP's example, Erlang gets that the right way round. Functions are functions are functions. If I want to call a function synchronously, I just call it. If I want to parallelise, then I simply spawn the function instead. > or should we just hope that the runtime/compiler gets smart enough to know what is safe to parallelize? That would be a step on further still. It's definitely doable - compilers already do a buch of pretty sophisticated dataflow analysis. But even today, the red/green async model is a very limited thing. With significant constraints. Other than perhaps rust - with its focus on zero-cost abstractions - it feels at best an interim step that asks programmers to compensate for a lack of good fine-grained concurrency support in language runtimes.
- gpderetta 6y agoA Cilk-style depth-first work stealing executor would block by default, but if any executor has free cycles it can steal the continuation and generate more work. You would need to wait only when work is actually needed or when returning from the function. Cilk requires annotating async functions calls at the call point (with the spawn keyword to make them async), but not the funciton themselves, so there is no red/blue coloring. Synchronization is explicit via the sync keyword, but in principle I think a compiler could insert the sync point on first use of the value. I don't think a cilk style runtime would be heavier than an executor behind async/await.
- acdha 6y ago> If you start over, there's no reason any blocking API should really BLOCK your program. Your functions can look completely synchronous, but yield to the event loop when they invoke a blocking API. This approach tends to become a lot less simple if you have any sort of significant resource usage to manage: you’ll be fighting all of that magic when you have non-trivial memory usage, need to avoid limits on file handles or connections, etc. not to mention the runtime support for things like debugging. Any time you see a long-running widespread problem and think there’s an easy fix, ask whether you’re really that much smarter than an entire field or whether it might be harder than you think.
- slver 5y agoI’m not sure what you’re referring to.
- acdha 5y agoIt basically comes down to the way async control flow interacts with resources which are in use. Say you have a classic program like this: db = connect_to_db() result = run_query(db, …) result2 = run_query(db, …) result3 = run_query(db, …) return build_response(result, result2, result3) If you make that async, you can make it potentially a good bit faster but you now have harder time reasoning about how many database connections are open at any point in time because it's not “one per thread” but rather a function of how many requests your async system allows to be inflight at any point in time. The same problem arises for memory usage: if you allocate memory it's going to stay allocated until the last async response arrives and if that's non-trivial it means that the async work might not be worth the hassle because your concurrency is actually RAM-limited below the point where you see any benefit over synchronous execution. One common challenge here is that it's usually quite easy to miss this being a problem no matter what style you use if you don't carefully test with real data volumes or under perfect conditions. For example, if my code looks like this: result_1 = run_query(db) result_2 = call_other_service() return build_result(result1, result_2) That'll run great during testing and async can make one of the calls effectively free but if either of those services gets slower all of the memory used for the intermediate state of the other calls still has to remain resident until it returns. I've seen this with databases and APIs, of course, but also unexpected things like auditd, syslog, directory services, NFS, etc. This is often reported as a problem with load-average / swapping because the system has a lot more requests in-progress than it typically does and wasn't tuned to keep that peak load within the physical capacity. There are, of course, ways to guard against this but then it's a lot harder to have a language make synchronous-looking code run asynchronously and leaving out async/await isn't saving you much if you have to sprinkle synchronization primitives all over the place. That doesn't mean you don't do it, of course, but it means there isn't a shortcut.
- pornel 6y agoRust was conceived as an Erlang-like language, and had "colorless" I/O and green threads in its early implementations. This idea has been demonstrated to be unfit for a systems programming language, and scrapped before 1.0: https://github.com/rust-lang/rfcs/blob/0806be4f282144cfcd55b1d20284b43f87cbe1c6/text/0230-remove-runtime.md https://github.com/rust-lang/rfcs/blob/0806be4f282144cfcd55b...
- marcosdumay 6y ago> Your functions can look completely synchronous, but yield to the event loop when they invoke a blocking API. Haskell does that, and it is great. But there is no chance at all that I would want anything like that in Rust. It would be completely out of place, and break the entire concurrency model. That would make the language unfit for any task I use it for today.