[HN Gopher] Why you might want async in your project ___________________________________________________________________ Why you might want async in your project Author : jdon Score : 102 points Date : 2023-09-09 18:17 UTC (4 hours ago) (HTM) web link (notgull.net) (TXT) w3m dump (notgull.net) | PaulHoule wrote: | I find async is so much fun in Python and meshes with the other | things you can do with generators but that is because I have the | reference collector cleaning up behind me. | | Looking back with like 30 years of hindsight it seems to me that | Java's greatest contribution to software reuse was efficient | garbage collection; memory allocation is a global property of an | application that can't efficiently be localized as you might want | a library to use a buffer it got from the client or vice versa | and fighting with the borrow checker all the time to do that is | just saying "i choose to not be able to develop applications | above a certain level of complexity." | airstrike wrote: | And Java's worst contribution to software was how painfully | slow and resource hungry most of the software written with it | tends to be... | | Your argument is looking at the advantages Java brought to | development speed and entirely disregarding runtime speed | fnord77 wrote: | java benchmarks are close to C's benchmarks; thousands of | times faster than python | cies wrote: | not in terms of memory usage and startup time. otherwise | it's quite fast. | jshen wrote: | I don't like Java, but you are completely wrong. | notamy wrote: | Any tool can be misused - the same comment could be made | about Javascript, PHP, Perl, C, C++, Python, really any | language. | charrondev wrote: | The only Java thing I work with is ElasticSearch (and in the | past other lucene based search tools like Solr). These can be | resource hungry depending on what your indexing but they are | also faster and more scalable than other tools I'd used | before. | fnord77 wrote: | the JVM is an underappreciated engineering marvel | cies wrote: | > underappreciated | | widely used though. not sure if that count for appreciation, | but i think it's one of the highest forms. | | it's not bad, not not great either. i miss proper sum types, | and it really lament the fact that static things are nearly | impossible to be mocked which prompts everyone to use DI for | everything instead of static. | codeflo wrote: | I agree that memory management can't be solved locally. The | situation in C++, where every library or API you use has a | different cleanup convention, that you need to carefully read | about in the documentation to even properly review a pull | request, is proof of that. | | I disagree that this criticism applies to Rust. For 99% of the | cases, the idiomatic combination of borrow checking, Box and | Arc gets back to a unified, global, compiler-enforced | convention. I agree that there's a non-trivial initial skill | hurdle, one that I also struggled with, but you only have to | climb that once. I don't see that there's a limit to program | complexity with these mechanisms. | meindnoch wrote: | >The situation in C++, where every library or API you use has | a different cleanup convention, that you need to carefully | read about in the documentation to even properly review a | pull request, is proof of that. | | Lol wut. The C++ resource management paradigm is RAII. If you | write a library that doesn't use RAII, it's a bad library. | Not a fault of the language. | moregrist wrote: | There's a lot of C++ code out there and a lot that | interfaces with C. | | RAII is one method of cleanup but it doesn't work in all | situations. One that comes to mind is detecting errors in | cleanup and passing them to the caller. | | So it's not right to call every library that doesn't use | RAII "bad." There are other constraints, as well. Part of | the strength of C++ is to give you a choice of paradigms. | codeflo wrote: | You have two choices. | | Either you write code with good performance, which means | that functions do take references and pointers sometimes, | in which case you do have all of the usual lifetime issues. | This is the proper way to use C++, and it's perfectly | workable, but it's by no means automatic. That's the | reality that my comment was referencing. | | Or you live in a fantasy land where RAII solves everything, | which leads to code where everything is copied all the | time. I've lived in a codebase like this. It's the mindset | that famously caused Chrome to allocate 25K individual | strings for every key press: | https://groups.google.com/a/chromium.org/g/chromium- | dev/c/EU... | dataflow wrote: | You're missing a bunch of very important stuff in that | page you linked to. See what they listed as the culprits: | | > strings being passed as char* (using c_str()) and then | converted back to string | | > Using a temporary set [...] only to call find on it to | return true/false | | > Not reserving space in a vector | | c_str() isn't there for "good performance" to begin with; | it's there for interfacing with C APIs. RAII or not, GC | or not, you don't convert to/from C strings in C++ unless | you have to. | | The other stuff above have nothing to do with C++ or | pointers, you'd get the same slowdowns in any language. | | The language has come a long way since 2014. Notice what | they said the solutions are: | | > base::StringPiece [...] | | a.k.a., C++17's std::string_view. | codeflo wrote: | I'm responding to a comment that claims all lifetime | issues are solved by RAII. | | My argument was that for efficient code, you need to pass | references or pointers, which means you do need to care | about lifetimes. | | And your argument is that's not true because we now have | std::string_view? You do realize that it's just a pointer | and a length, right? And that this means you need to | consider how long the string_view is valid etc., just as | carefully as you would for any other pointer? | dataflow wrote: | > I'm responding to a comment that claims all lifetime | issues are solved by RAII. | | I don't see anybody claiming this. The parent I see you | initially replied to said "the C++ resource management | paradigm is RAII", not "all lifetime issues are solved by | RAII". | | > My argument was that for efficient code, you need to | pass references or pointers, which means you do need to | care about lifetimes. | | Of course you do. Nobody claimed you don't need to care | about lifetimes. (Even in a GC'd language you still need | to worry about not keeping objects alive for too long. | See [1] for an example. It's just not a memory safety | issue, is all.) The question was whether "every library | or API you use" needs to have "a different cleanup | convention" for performance reasons as you claimed, for | which you cited the Chromium std::string incident as an | example. What I was trying to point out was: | | > that's not true because we now have std::string_view? | You do realize that it's just a pointer and a length, | right? | | ...because it's not merely a pointer and a length. It's | both of those bundled into a _single object_ (making it | possible to drop them in place of a std::string much more | easily), _and_ a bunch of handy methods that obviate the | ergonomic motivations for converting them back into | std::string objects, hence preventing these issues. | (Again, notice this isn 't just me claiming this. The | very link you yourself pointed to was pointing to | StringPiece as the solution, not as the problem.) | | So what you have left is just 0 conventions for cleanup, | 1 convention for passing read-only views (string_view), 1 | convention for passing read-write views (span), and 1 | convention for passing ownership (the container). No need | to deal with the myriads of old C-style conventions like | "don't forget to call free()", "keep calling with a | larger buffer", "free this with delete[]", or whatever | was there over a decade ago. | | > And that this means you need to consider how long the | string_view is valid etc., just as carefully as you would | for any other pointer? | | Again, nobody claimed you don't have to worry about | lifetimes. | | [1] https://nolanlawson.com/2020/02/19/fixing-memory- | leaks-in-we... | cma wrote: | 2014, isn't that pre-C++11 in Chromium? | jeremyjh wrote: | I agree that a lot of that happens in the real world. I | disagree that RAII is not used in the real world. I | worked on a very large codespace for ATM client software | and we used it pervasively, and the only memory leak we | had in my time there was in a third-party library which | ... required the careful reading of documentation you | mentioned. | Nullabillity wrote: | The problem with garbage collection is that it doesn't work for | other kinds of resources than memory, so basically every | garbage collected runtime ends up with an awkward and kinda- | broken version of RAII anyway (Closeable, defer, using/try- | with-resources, context managers, etc). | | Static lifetimes are also a large part of _the rest_ of Rust 's | safety features (like statically enforced thread-safety). | | A usable Rust-without-lifetimes would end up looking a lot more | like Haskell than Go. | eternityforest wrote: | Python handles all kinds of stuff with garbage collection. | | The problem is that things like closing a socket are not just | generic resources, a lot of the time nonmemory stuff has to | be closed at a certain point in the program, for correctness, | and you can't just let GC get to it whenever. | mplanchard wrote: | I don't think this is true. Context managers call special | magic "dunder" methods on the instance (I don't remember | the specific ones), and I'm pretty sure those don't get | called during regular garbage collection of those | instances. It's been a few years since I was regularly | writing python, so I might be wrong, but I don't believe | that context manager friendly instances are the same as | Rust's Drop trait, and I don't think their cleanup code | gets called during GC. | Nullabillity wrote: | Python is a fun case of "all of the above" (or rather, a | layering of styles once it turns out a previous one isn't | workable). | | Originally, they used pure reference counting GC, with | finalizers used to clean up when freed. This was "fine", | since RC is deterministic. Everything is freed when the | last reference is deleted, nice and simple. | | But reference counting can't detect reference cycles, so | eventually they added a secondary tracing garbage | collector to handle them. But tracing GC isn't | deterministic anymore, so this also meant a shift to | manual resource management. | | That turned out to be embarrassing enough that context | managers were eventually introduced to paper over it. But | all four mechanisms still exist and "work" in the | language today. | im3w1l wrote: | Are you saying that a finalizer is _guaranteed_ to run | when the last reference is deleted? So you could actually | rely on them to handle the resources, as long as you are | careful not to use reference cycles? | Nullabillity wrote: | In CPython 2.7, yes. In CPython in general, I believe | it's currently still the case, but I don't think it's | guaranteed for future versions. | | For Python in general, no. For example, as far as I know | Jython reuses the JVM's GC (and its unreliable finalizers | with it). | | It's also easy to introduce accidental cycles. For one, a | traceback includes a reference to every frame on the call | stack, so storing that somewhere on the stack would | create an unintentional cycle! | mplanchard wrote: | Wrote Python professionally for years and didn't know all | of this. Thanks! | pphysch wrote: | > The problem with garbage collection is that it doesn't work | for other kinds of resources than memory | | Why is that a "problem with GC"? | | Abstracting away >90% of resource management (i.e. local | memory) is a significant benefit. | | It's like saying the "problem with timesharing OS" is that it | doesn't address 100% of concurrency/parallelism needs. | pbourke wrote: | I quite like context managers and try-with-resources style | constructs. They make lifetimes explicit in the code in a | fairly intuitive way. You can get yourself turned around if | you deeply nest them, etc but there are usually ways to avoid | those traps. | ok123456 wrote: | Don't static lifetimes just mean that leaking memory is | considered 'safe' in rust? | Nullabillity wrote: | Static lifetimes as in "known and verified at compile- | time", not "the 'static lifetime". | nextaccountic wrote: | > basically every garbage collected runtime ends up with an | awkward and kinda-broken version of RAII anyway (Closeable, | defer, using/try-with-resources, context managers, etc). | | RAII works only for the simplest case: when your cleanup | takes no parameters, when the cleanup doesn't perform async | operations, etc. Rust has RAII but it's unusable in async | because the drop method isn't itself async (and thus may | block the whole thread if it does I/O) | paholg wrote: | There are workarounds. You could, for example, have a drop | implementation spawn a task to do the I/O and exit. | | Also, if your cleanup takes parameters, you can just store | them in the struct. | jayd16 wrote: | You make an interesting point. Has any language introduced a | generic-resource-collector? You're not supposed to use | deconstructors to clean up resources because you're left to | the whims of the GC which is only concerned about memory. | | Has anyone build a collector that tracks multiple types of | resources an object might consume? It seems possible. | jerf wrote: | Erlang is probably the closest. The word you want to search | for is "port". If it doesn't seem like it at first, keep | reading. It's a very idiosyncratic take on the topic of you | view it from this perspective because it isn't exactly | their focus. But it does have a mechanism for collecting | files, sockets, open pipes to other programs, and a number | of other things. Not fully generic, though. | francasso wrote: | Am I the only one that after reading opening sentences like | | "There is a common sentiment I've seen over and over in the Rust | community that I think is ignorant at best and harmful at worst." | | just refuses to read the rest? If you are actually trying to make | a point to people that think differently than you, why antagonize | them by telling them they don't know what they are talking about? | winwang wrote: | I agree with your sentiment, but want to point out the irony of | choosing ignorance after being insulted as ignorant. | francasso wrote: | You are assuming that the article would actually increase my | knowledge | quickthrower2 wrote: | Heuristics. Not going to read every article that negs me. | andy_xor_andrew wrote: | > I've written quite a few Rust projects where I expect it to | only involve blocking primitives, only to find out that, | actually, I'm starting to do a lot of things at once, guess I'd | better use async. | | In my experience (which, admittedly, is _far_ less than the | author, a developer of smol!) the answer to "I'm starting to do | a lot of things at once" in Rust is usually to spin up a few | worker threads and send messages between them to handle jobs, a | la Ripgrep's beautiful implementation. | | In a way, it seems like async Rust appears more often when you | need to do io operations, and not so much when you just need to | do work in parallel. | | Of course, you surely _can_ use async rust for work in parallel. | But it 's often easier to keep async out of it if you just need | to split up some work across threads without bringing an entire | async executor runtime into the mix. | | I don't think async/await was poorly implemented in Rust - in | fact, I think it avoids a lot of problems and pitfalls that | _could_ have happened. The complications arise because async | /await is, kind of, ideologically antithetical to Rust's other | goal of memory safety and single-writer. Rust really wants to | have its cake (compile-time memory safety) and eat it too | (async/await). And while you can criticize it, you have to admit | they did a pretty good job given the circumstances. | a-dub wrote: | do any of the async libraries for rust have good visualization | tools for inspecting the implicit state machine that is | constructed via this type of concurrency primitive? | mplanchard wrote: | Not sure if it's exactly what you're looking for, but tokio- | console is pretty nice | Arnavion wrote: | The state machine transformation is not specific to any async | libraries. The compiler is the one that desugars async fns / | blocks to state machines. AFAIK there is nothing other than | dumping the HIR / MIR from rustc to inspect it. But even | without that the transformation is pretty straightforward to do | mentally. | | The first transformation is that every async block / fn | compiles to a generator where `future.await` is essentially | replaced by `loop { match future.poll() { Ready(value) => break | value, Pending => yield } }`. ie either polling the inner | future will resolve immediately, or it will return Pending and | yield the generator, and the next time the generator is resumed | it will go back to the start of the loop to poll the future | again. | | The second transformation is that every generator compiles to | essentially an enum. Every variant of the enum represents one | region of code between two `yield`s, and the data of that | variant is all the local variables that in the scope of that | region. | | Putting both together: async fn foo(i: i32, | j: i32) { sleep(5).await; i + j | } | | ... essentially compiles to: fn foo(i: i32, | j: i32) -> FooFuture { FooFuture::Step0 { i, j } | } enum FooFuture { Step0 { i: i32, j: | i32 } Step1 { i: i32, j: i32, sleep: SleepFuture } | Step2, } impl Future for FooFuture { | fn poll(self) -> Poll<i32> { loop { | match self { Self::Step0 { i, j } => { | let sleep = sleep(5); self = | Self::Step1 { i, j, sleep }; } | Self::Step1 { i, j, sleep } => { | let () = match sleep.poll() { | Poll::Ready(()) => (), | Poll::Pending => return Poll::Pending, | }; self = Self::Step2; | return Poll::Ready(i + j); } | Self::Step2 => panic!("already run to completion"), | } } } } | klysm wrote: | In C#, I put anything doing IO in an async function and make | cancelation tokens required | Arnavion wrote: | >Except, this isn't a problem with Rust's async, it's a problem | with tokio. tokio uses a 'static, threaded runtime that has its | benefits but requires its futures to be Send and 'static. | | It's not a problem with tokio either. The author's point is | specifically about the multi-threaded tokio runtime that allows | tasks to be moved between worker threads, which is why it | requires the tasks to be Send + 'static. Alternatively you can | either a) create a single-threaded tokio runtime instead which | will remove the need for tasks to be Send, or b) use a LocalSet | within the current worker that will scope all tasks to that | LocalSet's lifetime so they will not need to be Send or 'static. | | If you go the single-threaded tokio runtime out, that doesn't | mean you're limited to one worker total. You can create your own | pseudo-multi-threaded tokio runtime by creating multiple OS | threads and running one single-threaded tokio runtime on each. | This will be similar to the real multi-threaded tokio runtime | except it doesn't support moving tasks between workers, which | means it won't require the tasks to be Send. This is also what | the author's smol example does. But note that allowing tasks to | migrate between workers prevents hotspots, so there are pros and | cons to both approaches. | vc8f6vVV wrote: | > Why don't people like async? | | That's pretty simple. The primary goal of every software engineer | is (or at least should be) ... no, not to learn a new cool | technology, but to get the shit done. There are cases where async | might be beneficial, but those cases are few and far in between. | In all other cases a simple thread model, or even a single thread | works just fine without incurring extra mental overhead. As | professionals we need to think not only if some technology is | fun, but how much it actually costs to our employer and about | those who are going to maintain our "cool" code when we leave for | better pastures. I know, I know, I sound like a grandpa (and I | actually am). | diarrhea wrote: | But async Python is a single threaded. I'd prefer async over | multithreading in python nowadays. Otherwise code can be slow | as piss, if it's doing a lot of I/O. Then, async is almost | table stakes for almost any level of reasonable performance | (GIL and all). | hgomersall wrote: | Your answer boils down to: "I know this technique, I don't want | to learn blub technique. My job is to get stuff done, not learn | new techniques." In which case, good for you; enjoy your sync | code (seriously), and please stop telling the rest of us that | have learnt the new blub technique that we shouldn't use it. | __MatrixMan__ wrote: | Another reason to is that it lets you handle bursty input with | bursty CPU usage. Sounds great, right? Round peg, round hole. | | But nobody will sell you just a CPU cycle. They come in bundles | of varying size. | | I recently heard a successful argument that we should take the | pod that's 99% unutilized and double its CPU capacity so it can | be 99.9% unutilized, that way we don't get paged when the data | size spikes. | | When I proposed we flatten those spikes since they're only | 100ms wide it was sort down because "implementing a queueing | architecture" wasn't worth the developer time. | | I suppose you could call it a queueing architecture. I'd call | it a for loop. | maxbond wrote: | Most commercial code is running an almost entirely IO workload, | acting as a gatekeeper to a database or processing user | interactions - places where async shines. | | Async isn't a lark, it's a workhorse. The goal is not to write | sexy code, it's to achieve better utilization (which is to say, | save money). | chrisweekly wrote: | FYI This is an interesting response by the maintainer* of smol to | this recent discussion: | https://news.ycombinator.com/item?id=37435515 | | * EDIT: corrected, thanks | Nullabillity wrote: | Maintainer, not creator. Smol was originally created by | stjepang, who has basically disappeared these days. | | EDIT: I originally incorrectly claimed that stjepang also | created rather than maintained crossbeam, making the same | msitake as I was correcting. | chrisweekly wrote: | whoops! thanks | urschrei wrote: | crossbeam was created by Aaron Turon (who has - inevitably - | also left the Rust project): | https://aturon.github.io/blog/2015/08/27/epoch/ | Nullabillity wrote: | Oops, shame on me! | aaomidi wrote: | I really wish we had stuff like the switch to scheduler that | effectively makes asyncish behavior possible at the kernel level. | | I'm tired of everyone implementing async on their own. | api wrote: | The author is a maintainer of smol, which I think is a far | superior runtime to tokio for numerous reasons including | structured concurrency, performance, size, and an ownership | parameter that reduces the need for Arc<> all over the place by | letting you scope on the runtime or task. The whole thing is just | tighter and better thought out. | | Yet tokio is the de facto standard and everything links against | it. It's really annoying. Rust should have either put a runtime | in the standard library or made it a lot easier to be runtime | neutral. | lawn wrote: | Is there a solid web framework tht uses smol instead of tokio? | nyanpasu64 wrote: | I'd argue that libraries forcing programs to include an async | runtime upfront because there's a chance that it may someday grow | to the extent you want a central executor (when they are | absolutely ill-suited for audio programming, and probably not the | case for Animats's metaverse client at | https://news.ycombinator.com/item?id=37437676), imposes | unnecessary dependency bloat on applications. And unless you use | block_on(), async code pushes applications from a "concurrency | library" model where apps block on mutexes/signals/channels as | needed, to a "concurrency framework" model where code only runs | under the runtime's callbacks (which is not the right choice for | all apps). | dpc_01234 wrote: | I might need a lot of stuff in my software. Eventually. I might | need distributed database, or to to scale it out to run on | multiple machines, and then maybe Raft, or reactive architecture, | zero-copy IO, or incremental updates or ... or ... the list goes | on and on. | | Thinking too much and in particularly going with over complicated | solutions from the very start because "might" is just bad | engineering. | | Also, even if I do need async in a certain place, doesn't mean I | need to endure the limitations and complexity of async Rust | _everywhere_ in my codebase. I can just spawn a single executor | and pass around messages over channels to do what requires async | in async runtime, and what doesn 't in normal and simpler (and | better) blocking IO Rust. | | You need async IO? Great. I also need it sometimes. But that | doesn't explain the fact that every single thing in Rust | ecosystem nowadays is async-only, or at best blocking wrapper | over async-only. Because "async is web-scale, and blocking is not | web-scale". | | Edit: Also the "just use smol" comically misses the problem. | Yeah, smol might be simpler to use than tokio (it is, I like it | better personally), but most stuff is based on tokio. It's an | uphill battle for the same reasons using blocking IO Rust is | becoming an uphill battle. Only thing better than using async | when you don't want to is having to use 3 flavors (executors) of | async, when you didn't want to use any in the first place. | | Everything would be perfect and no one would complain about async | all the time if the community defaulted to blocking, | interoperable Rust, and then projects would pull in async in that | few places that _do actually need_ async. But nobody wants to | write a library that isn 't "web-scale" anymore, so tough luck. | Ar-Curunir wrote: | > I also need it sometimes. But that doesn't explain the fact | that every single thing in Rust ecosystem nowadays is async- | only, or at best blocking wrapper over async-only. | | Now that's just plainly untrue. | dpc_01234 wrote: | Yes, yes. You're right. Though it does sometimes feel like | it. | the__alchemist wrote: | I also see the `Async` vs `blocking` false dichotomy in | embedded rust discussions. `Async/Await` != asynchronous | execution. | IshKebab wrote: | > Even the simple, Unix-esque atomic programs can't help but do | two or three things at once. Okay, now you set it up so, instead | of waiting on read or accept or whatnot, you register your file | descriptors into poll and wait on that, then switching on the | result of poll to figure out what you actually want to do. | | > Eventually, two or three sockets becomes a hundred, or even an | unlimited amount. Guess it's time to bring in epoll! Or, if you | want to be cross-platform, it's now time to write a wrapper | around that, kqueue and, if you're brave, IOCP. | | This feels like a straw man. Nobody is saying "don't use async; | use epoll!". The alternative to async is traditional OS threads. | This option is weirdly not mentioned in the article at all. | | And yes they have a reputation for being very hard - and they can | be - but Rust makes traditional multithreading _MUCH_ easier than | in C++. And I would argue that Rust 's async is equally hard. | | Rust makes traditional threading way easier than other languages, | and traditional async way harder than other languages, enough | that threads are arguably simpler. | conradludgate wrote: | It's necessary to use some kind of poll construction if you | want cancellation and timeouts without shutting down the entire | application | adamch wrote: | > tokio uses a 'static, threaded runtime that has its benefits | but requires its futures to be Send and 'static. | | This is only partly true -- if you want to `spawn` a task on | another thread then yes it has to be Send and 'static. But if you | use `spawn_local`, it spawns on the same thread, and it doesn't | have to be Send (still has to be 'static). | carterschonwald wrote: | Isn't the real problem the lack of monads and monad transformers | so you can't have async machinery used for domain specific stuff? | [deleted] | delusional wrote: | The author starts by citing greenspun's tenth rule and goes on to | elaborate on the argument that if you are going to have a half | implementation of async anyway, why not just pull it in? Yet | fails to interrogate the relationship between this argument and | the cited "rule". If you should use async because you might need | it in the future, shouldn't we all be writing in lisp? | | If we presuppose that all software eventually develops an async, | and we therefore should use async. Would it not stand to reason | that greenspun's rule that all software contains a lisp would | imply that we must also all use lisp? | orangea wrote: | The rule isn't really about lisp, it's about the kinds of | functions and structures you find in the standard library of a | typical programming language, such as strings and arrays and | file IO and so on. Rust already has those things so your | argument doesn't really apply. | Arnavion wrote: | The author said what you wrote in the first sentence, ie "use | async if you are going to have a half implementation of async | anyway". "Use async because you might need it in the future" is | something you made up, not what the author said. | delusional wrote: | greenspun's tenth rule is about the inevitability of the half | baked implementation of lisp. By evoking the sentiment of the | rule the author is implicitly making the argument that all | "sufficiently complicated programs" will eventually contain a | half baked implementation of async. | | The implicit argument doesn't stand alone though. The author | goes on to write: | | > It happens like this: programs are naturally complicated. | Even the simple, Unix-esque atomic programs can't help but do | two or three things at once. Okay, now you set it up so, | instead of waiting on read or accept or whatnot, you register | your file descriptors into poll and wait on that, then | switching on the result of poll to figure out what you | actually want to do. | | The implication is clear. Even simple programs will | eventually require async, and should therefore just use it | right now. unix-esque in this paragraph is supposed to evoke | ls or cat. Is your program really going to be simpler than | cat? No? Then you apparently need async. | Arnavion wrote: | >The implication is clear. Even simple programs will | eventually require async, and should therefore just use it | right now. | | There's no implication. Read what you quoted instead of | digging for quick jabs. "Even the simple, Unix-esque atomic | programs can't help but do two or three things at once. | Okay, now you set it up so, instead of waiting on read or | accept or whatnot..." | | >unix-esque in this paragraph is supposed to evoke ls or | cat. Is your program really going to be simpler than cat? | No? Then you apparently need async. | | cat and ls don't do two or three things at once. | dang wrote: | Recent and related: | | _Maybe Rust isn't a good tool for massively concurrent, | userspace software_ - | https://news.ycombinator.com/item?id=37435515 - Sept 2023 (567 | comments) | nevermore wrote: | Lots of comments and arguments about async being big or complex, | and it's really not, it's pulling in the runtimes that's big and | complex, and I think Rust really failed by forcing libraries to | explicitly choose a runtime. As a library developer you're then | put in the position of not using async, or fragmenting yourself | to just the subset of users or other libraries on your runtime. | eternityforest wrote: | I love async in Python and JS. I used to be one of those "Threads | aren't that hard, just use threads" people, but that was back | when the trend was doing "async" with layers of nested callbacks | like as if this was LISP or something where people just accept | deep nesting. | | Now we have async/await and I'm always happy to see it. | klabb3 wrote: | I mean.. I appreciate that there are proponents and people trying | to improve the state of async rust but to allude that everything | is dandy is either dishonest or more likely a strong curse of | knowledge bias. | | I've worked deeply in an async rust codebase at a FAANG company. | The vast majority chooses a dialect of async Rust which involves | arcs, mutices, boxing etc everywhere, not to mention the giant | dep tree of crates to do even menial things. The ones who try to | use proper lifetimes etc are haunted by the compiler and give up | after enough suffering. | | Async was an extremely impressive demo that got partially | accepted before knowing the implications. The remaining 10% | turned out to be orders of magnitude more complex. (If you | disagree, try to explain pin projection in simple terms.) The | damage to the ecosystem from fragmentation is massive. | | Look, maybe it was correct to skip green threads. But the layer | of abstraction for async is too invasive. It would have been | better to create a "runtime backend" contract - default would be | the same as sync rust today (ie syscalls, threads, atomic ops etc | - I mean it's already half way there except it's a bunch of | conditional compilation for different targets). Then, alternative | runtimes could have been built independently and plugged in | without changing a line of code, it'd be all behind the scenes. | We could have simple single-threaded concurrent runtimes for | embedded and maybe wasm. Work stealing runtimes for web servers, | and so on. | | I'm not saying it would be easy or solve _all_ use-cases on a | short time scale with this approach. But I do believe it would | have been possible, and better for both the runtime geeks and | much better for the average user. | biomcgary wrote: | Your position wrt green threads sounds like Graydon's | (https://graydon2.dreamwidth.org/307291.html). | fnordpiglet wrote: | Biggest issue I have with async is the lack of native async | traits and the lack of support for async closures. You can work | around the traits issue but the closure issue you can't. I've | spent hours trying to work around closures that wrap async code. | charcircuit wrote: | async is not free. It will turn your code into a big state | machine and each thing you await will likely create its own | thread. | | There is simplicity in a avoiding that and having code that gets | compiled to something that is straightforward and single | threaded. | [deleted] | conradludgate wrote: | This is not true for Rust. Await in rust builds a larger state | machine from the former. It does no implicit thread or task | spawns (unless the future you're awaiting does them | explicitly). | | Furthermore, async rust can be run single threaded | maxbond wrote: | This is true of all abstractions; if you don't need them, then | they'll make your program more complex and more painful to | write and maintain. | | Exercising judgement about when to use or shirk an abstraction | is a lot of what being a software engineer is about. | eternityforest wrote: | When does async/await ever make your program harder to | maintain? Maybe to people who don't already know it, but | almost all the big languages have async, it would be hard for | a programmer to get away with not learning it, at least if | they're a python of JS programmer. | | It adds complexity, but it's at the level where you don't | have to think about it. If you're doing something advanced | enough to where async is a leaky abstraction, you're probably | doing something big enough to where you would want the | advantages it offers. | | If you're doing something simple, async is just a black box | primitive that is pretty easy to use. | api wrote: | Awaits don't create threads, at least not in any runtime I know | of. There is usually a fixed number of threads at launch. | tomwojcik wrote: | FastAPI docs, case when you don't create an async route | | > When you declare a path operation function with normal def | instead of async def, it is run in an external threadpool | that is then awaited, instead of being called directly (as it | would block the server). | | https://fastapi.tiangolo.com/async/#path-operation-functions | | OP either meant this, or its variation, such as async_to_sync | and sync_to_async. | https://github.com/django/asgiref/blob/main/asgiref/sync.py | | Ofc this is a python example. I have no idea how it works in | different languages. | maxbond wrote: | NB: In Python >= 3.9 the idiomatic way to do this is | to_thread(), not familiar with these ASGI functions but I | would guess they're a polyfill and/or predate 3.9. | | https://docs.python.org/3/library/asyncio- | task.html#asyncio.... | pdhborges wrote: | They are not polyfills. Multiple scheduling modes are | provided for libraries that are not thread safe (it's a | total mess and I avoid these wrappers like the plague) | [deleted] | mplanchard wrote: | "run in a threadpool" isn't the same as creating a thread | though | rugina wrote: | Tokio uses a pool of threads for disk I/O because it uses the | synchronous calls of the operating system. | charcircuit wrote: | That is an implementation detail on where you put the code | that is blocking or running concurrently from the main code. | An executor could use a separate OS thread, or the | application could itself schedule application levels threads | onto a number of OS threads. | | When writing a Future that will block for 5 seconds you will | need to find somewhere to that you can put the code to block | for 5 seconds. You don't technically need to even use an | executor here. | maxbond wrote: | I think they meant it was likely to spin off additional | tasks/green threads. | Arnavion wrote: | If they meant that they are still wrong. ___________________________________________________________________ (page generated 2023-09-09 23:00 UTC)