Alloy: Garbage Collection for Rust

I saw this on Hacker News

I am wondering what people think?

Especially async/await devotees. This could really help with some of the very difficult memory management problems.

I was very interested to see this and the video here.

Would a garbage collector help?

Can you elaborate a bit on what you're asking? I don't really see async cancellation, async drop, and garbage collection as being all that interdependent.

The garbage collector results are certainly interesting, but its direct integration with rustc makes it much harder to actually adopt without buy in from the rust project.

3 Likes

My POV is that of a language user, not designer.

Having used async/await in Rust and in Dart or Typescript the difference is startk.

In Rust I am forever having to use "magical incantations" to get async/await to work, to the extent I do not use it for asynchronous programming, it is too much trouble. In Typescript and Dart I have had none of those problems

Watching the talk on cancellation makes me think that Rust is going down the wrong track here.

My experiences using async/await in those three languages makes me think that an important part of the puzzle is a garbage collector. Trying to manage memory, as we do in Rust, without control over what code is running, as we do not with async/await, seems like an impossible task.

So I wonder if Rust should bifurcate and async/await should add garbage collection to the runtime? Would that heal the problems with memory management and cancellation?

I do a lot of asynchronous programming, in C back in the day, and in Rust now, mostly with callbacks and/or state machines, and I do not see what the problem is that async/await is trying to solve. It does not seem hard to me (callbacks and state machines). I think that puts me in a minority.

I am distressed that asycn/await is becoming the default both where asynchronous programming makes sense and where it does not.

That would be a completely different language IMO.

I don't see what GC has to do with async cancellation.

6 Likes

My experience, mainly with C# is the opposite. Yes, the GC makes things a bit easier but it is very very easy to introduce subtle concurrecy bugs between tasks running on different threads: nothing in the language or in the implementation of async helps you - all locking and synchronization should be done by hand. Rust just works.

I don't know how introducing a GC would impact Rust async/await but I fear that having long-lived objects and non-deterministic Drops would make things more complicated, not simpler.

1 Like

I'm curious as to what troubles you have and what "magical incantations" you have to use. Any little examples? I ask because I have yet to find writing async Rust harder than writing sync Rust.

Except perhaps when interfacing the async world to the sync world. But that was a problem for me in Python as well.

Similarly, I have yet to see that problem. Any examples?

3 Likes

Yes.

Async/await rust is half way there already - which is my point

With a garbage collector the GC would handle the cancellation, would it not? I am seriously out of my depth, so I may have the wrong end of that stick.

When I say "garbage collector" I am not being specific, I am being general about language run-times. Async/await already requires a run time which is how the programmer loses control of the code that is actually being executed. It is not the only way to loose that control, but loss of control is a feature, not a bug, of sync/await. I am proposing going the full distance and adding more resource management to the run time.

It would mean giving up on async/await real time programming or async/await embedded programming, but that is a tiny tiny fraction of code being written. For most use cases it would be much better if my experience of the three languages is any guide.

That right there is a show stopper. For me at least. In my world real-time and or embedded is very important. It's one of the main reasons I even considered to look at Rust in the first place. "systems programming" and all that.

Real-time and embedded Rust may be as tiny fraction of code being written at the moment but it is rapidly growing as far as I understand. It's important.

More generally, in a hand waving way, I don't see the point of trying to make Rust into another Java or C#. If they are what you really want they are very much still there.

9 Likes

Also, as a fraction of code being run in the world, embedded vastly outnumber "normal" computers. For a start, every "normal" computer or phone has multiple embedded microcontrollers in them: SSD controller, fan controller, keyboard controller. Wifi/lan/cellular all have at least one controller as well each. And a bunch more.

Add on top of that: washing machine, dishwasher, fridge, many microcontrollers in cars, trains, airplanes, etc. And then there is all the code that is realtime but not running on microcontrollers: industrial control systems, traffic lights, etc.

So in a very real sense, this segment is the most important in programming. Especially since if a game crashes, it may annoy users but doesn't really matter in the grand scheme of things. If the break controller in you car freezes there is a significant risk of death.

Embedded is in fact "most use cases"

6 Likes

Seems likely, or at least a huge percentage of use cases.

At the other extreme data centres are huge and devouring enormous amounts of energy. The need for efficiency is increasingly pressing there as well.

2 Likes

The issue is not really handling the cancellation, it's cancelling at the right point and cleaning up afterwards. This isn't something that a GC or runtime can do for you.

It would mean having two incompatible languages in one. You would have to make sure that the GC and runtime can be disabled. People will want to make sure that their dependencies don't enable the runtime/GC. The runtime implementation will have to make sure it doesn't break the assumptions that unsafe code make and so on.

6 Likes

That is what we have.

I want two incompatible languages.

Ok. Still out of my depth, but I thought that is exactly what a run time environment can do for you? I thought it was what people in the asyc/await world want from Rust's async/await runtime.

You didn't elaborate on this one; the only ones I can think of is:

  • needing to box in some cases like to get a type erased or recursive async function
  • select! macro, which is honestly both rare and much nicer than the equivalent code in GC languages I've used
  • pinning, which is not necessary for normal async "business logic" and relatively straightforward when you do need it (to use, understanding is a whole different ball game)
  • pin projection when implementing some funky combinator or integration, not necessary unless you're writing really low level code that you probably can find in plenty of libraries
  • sync/async interop, which is pretty much just spawn_blocking() and block_on()

So that's a bunch of things that are about as easy as a GC language, and a bunch of others you basically never do unless you're doing something impossible on those languages.

What am I missing?

Regarding drop in async (not the same thing as async drop, which might be some of the confusion!) - I think you're talking about the system writing to a user provided buffer after an async task is dropped, right? The problem there as I understand it isn't freeing the buffer after the IO completes and the OS is done using it, you can just use a box for that and that's what async io libraries already do[1], it's that Rust wants to let you cancel an async write without using a separate heap allocation. I believe the current approaches are things like the proposed (actual) async drop which would make drop points await points in some contexts, but there's a lot of fiddly language issues to work out and may not even be the right approach.


  1. actually I think it's that they move the buffer in and out, which lets you pick if it's on the heap or if you need to copy the data ↩︎

That is quite a bit, do you not think?

Less systematically and I am unsure what this means, this is a function signature I required to get a HTTPS server handling requests (it has been a while since I looked at this code, I am still a bit traumatised)

        let service = make_service_fn(move |_: _| {
            let data_server = Arc::clone(&data_server);
            async move {
                Ok::<_, Infallible>(service_fn(move |req: Request<Body>| {
                    let ds = Arc::clone(&data_server);
                    async move { Ok::<_, Infallible>(ds.process_request(req).await.unwrap()) }
                }))
            }
        });

I am unsure if that horrible interface is due to async/await or something else. But for a very simple process it is a l;oit of magic words

Not sure it is that rare? Whenever you need something that should process streams of data (be they proper Streams or just channels or something else) from multiple sources you run into select!.

They also come up when dealing with timeouts, which is extremely common.

https://rfd.shared.oxide.computer/rfd/400 for example describes async cancelling and the issues around that, select! is one of the primary sources of issues.

I would agree though that the alternatives are not better.

It's a list of everything I could think of, you'd almost never run into any of it past Box and maybe pin and select!

That example is somewhat bad, true, but it's mostly just tower/hyper being overly generic as a set of glue between all the different "processing" libraries out there (and also that it predates a bunch of nice trait features being added to the language), and not really anything to do with async itself.

That said, that example can easily be cleaned up, the outer make_service_fn is just Shared and you might not even need it at all depending on your context (see the current hyper server example). The inner one could be an implementation of Service on the data_service itself (or actually an Arc of it, I think), that's kind of the point of tower being so generic.

Of course, all of that completely disappears if you just use axum or something.

The underlying cause is that it's exposing you a bit too much to the internal logic of a server: in a loop accept connections, then for each connection in a loop process requests. All of those can handle failure and need to outlive their creator, so you get the wrapping results and clones and inner async moves. It could easily make the call for you on how those work, but then it wouldn't be the maximally generic solution it's trying to be.

If you want to say that GC would make that exact example simpler, sure, but only very slightly and not any differently to most other examples in rust where you clone or need to think about callback capture lifetime.

1 Like

I mean that generally there's already a nicely packaged up solution, probably in futures. Occasionally getting the right combination is more complicated than just using a select!, but you're probably doing something a bit clever in the bad way in those cases anyway! :wink:

You are not doing “a very simple process” here, though. That's “complicated process made deceptively simple in other languages”. So it's not lack of magic words that does you, but more of the need to describe the same thing to the compiler, again and again. IOW there are not enough magic words.

It's like complaining that in C “a very simple string manipulation” can be a dozen of lines where most of higher-level languages offer syntax sugar.

There are bunch if ideas floating around that may help with that, like explicit capture clauses or Alias types… the big problem is not even that it's too hard to create, but that there are a lot of people (me included) who want to see these “magic words”—because to me they are something very important.

But I can understand that “we don't want to fix errors in our programs, we just want them to run”[1] folks can see it like you do… and there are an attempts to answer, but I'm not sure splitting the language down the middle into two incompatible camps is the answer… there are already bazillion other languages that already made that choice in favor of “we just want them to run” position… why would we need another one?


  1. Phrase describing difference between Algol and FORTAN customers in the billion dollar mistake Tony Hoare presentation. ↩︎

Important rule in Java was never manage connections, file handles, listeners, etc via finalize (drop) method. Rust style memory management drops objects immediately when out of scope. With garbage collector, it is not clear when the finalizer will run.

1 Like