Hacker Newsnew | past | comments | ask | show | jobs | submitlogin

If you want golang in Rust just use channels and tasks? Or threads?

I don't find async Rust difficult at all, I'm having a hard time really empathizing with this to the extent of needing breaking changes.

To me, async from a lang perspective is virtually done - in 2024 I suspect all of the various impl Trait and async Trait stuff will be done and at that point I don't see anything left.



I do that of course, and that's one of the easiest ways to use async Rust. In real projects you need much more however. F.ex. I had to code an example of how to add tasks to an already running pool of tasks and posted my findings here: https://github.com/dimitarvp/rust-async-examples/blob/main/e... (there's #2 as well with some more comments and a different approach).

The fact that I needed to make a GitHub repo and start making show-and-tell demos on how to do various things with async Rust to me is both a red flag and me being diligent BUT it should be more obvious. And promoted in docs.

Rust started suffering from "you got all the nuts and bolts in place, now build your own solution, son" syndrome which I grew to dislike. Too low-level. I wouldn't mind something akin to e.g. Golang's flowmatic library (check the first two examples at the top of the README): https://github.com/carlmjohnson/flowmatic


`flowmatic.Do` is equivalent to `future::try_join` (https://docs.rs/futures/latest/futures/future/fn.try_join.ht...), and `.Each` is equivalent to `stream::iter().map().buffer_unordered()` (https://docs.rs/futures/latest/futures/stream/fn.iter.html, https://docs.rs/futures/latest/futures/stream/trait.StreamEx..., https://docs.rs/futures/latest/futures/stream/trait.StreamEx...).

`.ManageTasks` doesn't seem to have a clear analogue that I can think of right now, that's an interesting one.

`.TaskPool` looks like `StreamExt::buffer_unordered` unless I'm missing something. The code example in the README didn't really say a lot for someone who isn't already well-versed in the library.


That's on me for making blanket statements -- my apologies. Yep, I know about the first two and I used them regularly.

My issue in general is that async Rust involves using several things at the same time (not just the two keywords) and this should be surfaced much earlier in any intro material and the maintainers of the language (or the libraries, or both) should just double down on whatever they feel is the best way to do async Rust.

The stance of "you have freedom, make your choice and assemble your own LEGO" is not very productive. I want Rust to start being a bit more opinionated.

Though I'll recognize this is just a personal taste but I still have to defend it by saying that as a programmer who is paid to deliver, I want to be able to deliver in predictable timelines and not having to go off on an adventure to learn all the intricacies of async Rust before I am able to write a feature.


I'd like to think that I've worked on some "real projects" and I've never run into these issues tbh. It's just hard for me to wrap my mind around. Like I said, I get wanting more libraries (although I think they exist? A local pool exists and you can spawn tasks in it, using tokio) but I'm just not seeing a fundamental problem.

I found the code you wrote very confusing so I can see why, when you come back to it, you find it confusing. But I don't really understand its purpose either.


> but I'm just not seeing a fundamental problem.

Well, I mentioned it a few times in this thread: things are mixed in non-intuitive ways -- async/await, tokio, and various crates (including StreamExt which was not at all obvious and it took me a while to finally find mentioned in a few blog posts and GitHub gists). It just introduces friction and nowadays I am more averse to tech that requires more homework. Admittedly a personal preference of course but I don't think it's an invalid concern either.

> I found the code you wrote very confusing so I can see why, when you come back to it, you find it confusing.

Well, I needed such a pattern in a project that had to start as quickly as at all possible and only start and schedule the most critical work -- and then start adding all the other work as the first batch of work was already in progress.

Would you write it differently and if so, how?


> Would you write it differently and if so, how?

It's really hard to say because I don't fully know your use case. Maybe your way was the right way but I think I probably would have done something differently. Maybe a priority queue of jobs with a static pool of workers.


Yeah, fair enough, I admit I haven't dabbled with any Rust job queues yet.


> The fact that I needed to make a GitHub repo and start making show-and-tell demos on how to do various things

While I resonate with this, because I also went through a period of struggling with async stuff, I genuinely think this is because async is just hard in general. Done properly, it yields an immense amount of power, quite efficiently, but also opens up a lot of “degrees of freedom” about it can be operated, which leads to confusion.

A lot of the async stuff, how it actually worked, and how to actually use it, only clicked for me when I played around with Glommio, which runs an executor-per-core, and some of the constraints it imposed made understanding it all somewhat easier.


Thanks a lot of the Glommio mention, that's an instant star and I'll review it in more details Soon™.

> Done properly, it yields an immense amount of power, quite efficiently, but also opens up a lot of “degrees of freedom” about it can be operated, which leads to confusion.

Yeah, very well put. Indeed it's very powerful and sure it's confusing. I need guard rails. And I need my hands slapped much more. "Looks like you're trying X -- this is how you do it, you idiot" would work well. :D

I am almost not joking even.


I think this blog post by tomaka gives a good summary of the current issues with async rust. Certainly much more comprehensive than what you can write in a hacker news post:

https://tomaka.medium.com/a-look-back-at-asynchronous-rust-d...

It highlights some of the same problems:

- “Just spawn a new task”

- The Send trait isn’t what it means anymore


I guess I just don't run into these issues. Maybe because I write my code to be effectively single threaded already. I almost never actually need a channel, mutex, etc, except in very specific and scoped areas.

I wonder why I haven't hit these issues despite writing web services in Rust for years.


Sounds to me like you are dealing with this at the process boundary, more like actor and message based software. This is a mature approach and will serve you well.

The typical context for stuff like async/await is code that should be running in a separate process so it can block but doesn't. Then it gets ugly fast.


Yes, I suspect you're right. I do not like building giant monoliths with internal task systems. I prefer using async/await on the inside and microservices with rpcs or, ideally, streams for communication.

I think perhaps people are trying to push too much into a single process?


> I think perhaps people are trying to push too much into a single process?

That's exactly what's happening. A bit of Erlang/Elixir experience would help a lot of people to create architectures (even in other languages) that do not require such kludges. If your application starts to take on the kind of complexity that you normally deal with a monolithic OS kernel you're doing something terribly wrong.


And then people are saying that their microservice is high performance because it's handling 10 000 req/s per 72 core node... which is ok but rather per core (depending on the amount and type of logic being processed), not node. I'm handling requests in high microseconds range (median) in high volume, low latency network service with hard limits on latency numbers (not HFT). Good luck building that kind of service (a lot of constantly changing state) with only microservices with less complexity and higher throughput then in monolith or semi monolith with a lot of internal state guarded by different synchronization mechanisms. I/O overhead alone would eat you.


Any design pattern can be applied in a wrong way.


Sure it can, but all design patterns have their use cases. Generalizing and saying that one is just "terrible" (which your previous comment implied), and people "should learn" is just untrue and an ignorant take. Sometimes it's the only sane solution compared to others, considering project constraints.


Yes, that one is terrible compared to the alternatives. If there is one thing that really irritates me about the way we go about IT then it is that stuff that works and is reliable and well understood gets tossed and replaced by 'new shiny thing' which then eventually undergoes the same treatment. We are eternally stuck in reinventing wheels, rather than that we make real progress in for instance reliability and are able to accept liability for what we create.

Reducing scope and simplification, increasing reliability and ultimately aiming for software as a true engineering profession should be our goal. Not fashion and running after certain other eco-systems because they are perceived to be in competition. Rust has promise, it is potentially a game changer and this sort of distraction can be done without. It feels as if over time the Rust project is being hijacked by people that want it to be everything for everybody, and now places itself not only in competition with C (and possibly C++) but also with Node (which is itself in competition with C).

Especially considering project constraints of the Rust project it might pay off to see what ultimately is what makes C++ a problematic language. This can be traced back to Stroustrup dragging in everything and the kitchen sink rather than to stick to his original vision, which was 'C but with classes'. The end result is a giant hairball and fifty (ok, exaggerated) ways of achieving the same thing.


On first note, yes that is what happens when a language whose original scope was to be a systems programming language, and now wants to do everything.

Regarding Bjarne Stroustrup, he wasn't dragged anything, his C++ is described in "The C++ Annotated Referece Manual" and "The Design and Evolution of C++", everything that happened after that is responsability of WG21, where he only has one vote among 300+ persons.

There are several papers from him where he complains about the direction WG21 is going, like "Remember the Vasa".


Hm, ok, I admit that bit was from memory, the quote that stuck was 'C++ now has as many additions and ways of doing things as there are ways of pronouncing 'Bjarne Stroustrup'', and stems from the days when his contributions were still much larger, say 20 years ago, whereas the 'remember the Vasa' quote is much more recent.

https://www.theregister.com/2018/06/18/bjarne_stroustrup_c_p...

I positively loved C++ when it was still called 'cfront' and after that it became more and more layers of complexity and tricks. C already had plenty of that (for instance, the weird pointer to a function syntax) and C++ went all out to drag in the various concepts that were present in other programming languages. This wasn't the same as the way say English borrows words and concepts from other languages freely, those are mostly just words and new ways to combinate existing ones. It would be as though English suddenly started using Kanji or logograms, or as if it would start using right-to-left writing or maybe even bottom to top in the middle of 'normal' sentences.

Such inclusivity increases the surface area of the language itself and ultimately makes life harder for everybody using it: you have to learn more because someone else could use these new constructs in their code and you may end up having to interact with it.

I think GvR got that one right with his 'there should be only one way to do it' but even that became a straightjacket.

I'm not saying I have all of the solutions and if Stroustrup wasn't the driving force behind C++'s complexity then I'll be happy to take that back. But I really believe that programming languages should strive for simplicity, just enough to make it all work so that the barrier to entry is small and the amount of gate-keeping can be kept to a minimum. That is the only way to really drive adoption and to hopefully undo some of the crazy fragmentation that we have in the programming language landscape.


The only languages capable of holding down to that simplicity require having some BDFL.

The moment the language evolution is driven by some kind of request for improvement, or similar process, with votes on what goes in or not, the outcome is design by commitee.

Many other languages aren't much better than C++, if you check their current versions and the Features/Year rate, including standard libraries.

Also having BDFL only works out as long as it is the original author, afterwards there is no guarantee that the successor has the same vision for the language.

The tragic long term meaning is to either accept complexity, or reboot the programming language ecosystem every couple of decades.


> Yes, that one is terrible compared to the alternatives

No, that's exactly opposite of what I meant when I described what I'm working on, that all of the alternatives that you both with OP described are terrible in that specific case because of project constraints. I actually measured that!

As to the rest of your comment I fully agree, tired of all the hype cycles myself.


> I think perhaps people are trying to push too much into a single process?

I'll immediately spin that right back at you: people are trying to do too much via too many OS processes. ¯\_(ツ)_/¯

I like my single-OS-process apps that can distribute work internally (via the aforementioned Erlang "green threads"; they are not exactly that but close), or Rust's tokio, or Golang's goroutines and channels and WaitGroups.

I suppose we can all retreat to our corners, shrug and say "well, it's just how I prefer doing things" but I still find the many-OS-processes approach overrated and leaky. You can optimize the program pretty well but then you have to worry about whether the kernel will schedule and distribute work properly (i.e. on a 24-core server, will spawning 24 copies even with CPU core pinning work well?).

I personally prefer to dispense with these worries and just make self-contained apps / services and then be able to put them in any hosting, and deal with scaling issues either by further optimizations, or just bumping the server bill with $20.


I don't think there's a meaningful difference between an Erlang actor and an operating system process other than the upfront memory allocation size.

The salient different to me is the supervisory structures. In Erlang you must compose them, in operating systems the kernel does that for you, or you move the responsibility into a distributed queue, etc.

As for scale, I don't really know how to reconcile "I'm fine just paying another 20 dollars" with "I'm worried about how my kernel will schedule my processes".


> in operating systems the kernel does that for you

Does it really? I am not an uber Linux expert but I have only heard of systemd doing that. Serious question, I'll appreciate more examples.

> As for scale, I don't really know how to reconcile "I'm fine just paying another 20 dollars" with "I'm worried about how my kernel will schedule my processes".

Sorry if I was unclear, I meant it as "I don't want to worry if the kernel will properly schedule N clones of my single-threaded program so I prefer to write a fully async one in a single OS process and work on it to make it efficient instead; failing that, I'll just bump my hosting so my program has the throughput I require".


The kernel is ultimately the thing that preempts your process and allows it to be killed, restarted, etc. It's also what handles things like "the connection was dropped", or "the thing timed out", etc. Userland facilitates management of those actions in a user oriented way through helpers like systemd or your container orchestrator or whatever.

It doesn't make much difference though, you can say it's systemd, the kernel, dockerd, or whatever. The point is that operating systems have tons of facilities built in for the management of processes.

> Sorry if I was unclear, I meant it as "I don't want to worry if the kernel will properly schedule N clones of my single-threaded program so I prefer to write a fully async one in a single OS process and work on it to make it efficient instead; failing that, I'll just bump my hosting so my program has the throughput I require".

Why not "I don't want to worry about if my kernel will properly schedule N clones of my single threaded program so I'll just bump my hosting so my program has the throughput I require" ?

Also I didn't suggest single threading, I generally build things as 'async/await' on the inside and 'service oriented' on the outside. So when there's some new domain I don't spawn a new task, I just spawn a separate process.


Well, it seems we're much more aligned than I thought. :D

> Why not "I don't want to worry about if my kernel will properly schedule N clones of my single threaded program so I'll just bump my hosting so my program has the throughput I require" ?

Boring answer: I never prioritized that way of work, I admit. And after gaining tons of experience in several ecosystems and languages I got burned out and right now I can't bring myself to learn systemd properly.

> Also I didn't suggest single threading, I generally build things as 'async/await' on the inside and 'service oriented' on the outside. So when there's some new domain I don't spawn a new task, I just spawn a separate process.

Ah, I see. Fair, I would and have done the same (though that comes with some care when coding the thing so it can determine what it needs to do so spawning the 2nd copy doesn't step on the toes of the first one).


> In Erlang you must compose them, in operating systems the kernel does that for you

Erlang's supervisor trees are much more powerful than anything the kernel will do for you.


Disagree. Processes and actors are nearly identical, except that the kernel has the ultimate ability to preempt everything.

Supervisors are built on `link`, and every process in erlang must always exit with an exit status. This is identical to a process handle with a process exit status.

The whole point of supervisors is that they are extremely simple - you can implement all of Erlang's OTC and supervisory semantics with, more or less, recursive functions (which are the foundation of actors), names, and links.


They are superficially functionally identical, but OTP (which is what I think you meant when you wrote OTC) goes a lot further than just some syntactic sugar. It is effectively a part of a distributed operating system that focuses on reliability at the cost of some other factors. It does that one thing and it does it extremely well with a footprint that can span multiple pieces of hardware. No operating system comes close to delivering that kind of reliability. You can cobble something like it together from a whole raft of services but why would you, it already exists.

Joe Armstrong's view of the software world (one with which I strongly identify so forgive me the soapbox mode) is that reliability, not throughput should be our main focus and that failure should be treated as normal rather than as exceptional and that distributed systems are the only viable way to achieve the kind of reliability that we need to bring software engineering into the world. This is a completely different kettle of fish than stringing together a bunch of services using kernel or auxiliary mechanisms, both in terms of results and in terms of overhead.


IDK how I typo'd OTC lol, sorry about that.

What you're talking about is the fact that Erlang as a VM is well built. That is irrelevant to whether or not supervisory trees are equivalent to what an OS provides.

> No operating system comes close to delivering that kind of reliability.

Erlang runs on an OS though. Like, Erlang would inherently always be limited by the reliability of the underlying system and in fact relies entirely on it for preemption.

> (one with which I strongly identify so forgive me the soapbox mode)

For the record, I am an extreme fan of Joe's and I routinely re-read his thesis, so you are preaching to the choir in a way.

> This is a completely different kettle of fish than stringing together a bunch of services using kernel or auxiliary mechanisms, both in terms of results and in terms of overhead.

I just totally disagree and in fact you'll find that Erlang's seminal works used the term 'processes' and in fact is based on the entire model of OS processes. Off the top of my head he cites at least two papers about processes and transactions as the foundation for reliability in his thesis.


Yes, but those processes are much lighter weight than OS processes and can be created and destroyed in a very small fraction of the time that the OS does the same thing.

> Like, Erlang would inherently always be limited by the reliability of the underlying system and in fact relies entirely on it for preemption.

This is factually incorrect, sorry. The Erlang VM uses something called 'reductions' which preempt Erlang processes when their computational slice has been reached which has absolutely nothing to do with the OS preemption mechanism.

And an Erlang system can span multiple machines with ease, even if those machines are located in different physical locations.

Erlang processes are more along the lines of greenthreads than full OS processes but they do not use the 'threads' mechanism the OS provides for the basic scheduling. The VM does use OS threads but this is mostly to decouple IO from the rest of the processing.

Oh, and BEAM/Erlang can run on bare metal if it has to.


> Yes, but those processes are much lighter weight than OS processes and can be created and destroyed in a very small fraction of the time that the OS does the same thing.

Yes, that is true. I don't think that is relevant to the semantics of the constructs.

> The Erlang VM uses something called 'reductions' which preempt Erlang processes when their computational slice has been reached which has absolutely nothing to do with the OS preemption mechanism.

That preemption relies on the runtime yielding at every function call. The only thing that can actually preempt a process mid instruction is the kernel, and actually it can't do that either it's the hardware that yields to the kernel.

> And an Erlang system can span multiple machines with ease, even if those machines are located in different physical locations.

Yes, that's not unique to Erlang, obviously one can launch processes on arbitrary machines.

> Erlang processes are more along the lines of greenthreads than full OS processes but they do not use the 'threads' mechanism the OS provides for the basic scheduling.

I think you're getting hung up on implementation details. The abstraction is semantically equivalent. One might be faster, one might be heavier, one might have some nice APIs, but in terms of supervisory primitives, as I said before, the only thing required is `link` and processes have that.

I'm too lazy to go to the various paper Joe cites but if you take the time you'll find that many of them are about processes




Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: