Green threads and runtime

Hi,

I have a question regarding the vision that Mojo’s team and Chris may have regarding offering a UX similar to Go’s go symbol.

In Rust, they opted over there to not have a runtime at all, and Tokyo became the de-facto runtime if you want to do concurrent programming, and because of that, there’s a lot of API duplication between std and Tokyo (mutex types is the example that comes to mind right now)

Mojo already has the pixie dust necessary to alternate between Mojo’s runtime and CPython runtime, and therefore, in my naïvité, I could see Mojo bringing a third runtime especially crafted for Go’s style of green threads.

Popping the stack, I guess, the question is indeed whether you folks see green threads as a feature of the language (Go) or as a service provided by an external library (Tokyo).

Thank you,
Ulderico

1 Like

Current view is to have zero cost async similar to Rust. There are discussions ongoing on whether or not colorless functions are feasible. Most likely async support on Mojo would not be like go as the community wants to avoid an async runtime if not being used.

Like Rust, Mojo is also aiming for the most performance and efficiency, not to mention deep integration with C/C++.

Mojo can not afford to do go-style async. Goroutine stacks uses more memory, introduces a lot of complexity, and the runtime makes C integration less trivial. It also differs significantly to Python async.

So stackless coroutines it is

I’m one of the people working on async for Mojo (at least the design part).

Go has a lot of design decisions built around goroutines. For instance, goroutines can outlive the goroutine which spawned them, since there is no guarantee when they finish. This is part of why Go has a garbage collector, because without that any value passed between greenthreads needs to last forever. A garbage collector is not an option in Mojo because Mojo has to run on GPUs, FPGAs, and other exotic processors which are incapable of running a garbage collector. If we were to go the goroutine route, Mojo would need to either abandon the borrow checker, meaning moving back to memory unsafety (which is discouraged) or give up running on GPUs, which is a lot of the point of Mojo.

As a go programmer, I’m sure you’ve done something like this rather frequently:

ret_ch := make(chan foo)
go do_thing(ret_ch)
// do a bit of parallel work
ret := <- ret_ch

Go wholly disconnecting the spawned processes causes a lot of headaches, and if you try to use them together with lifetimes or origins it gets much, much worse. This is part of why Rust removed greenthreads.

Goroutines also have issues with where they are stored. This means that, for a systems language, Mojo would need to have manual control over where goroutines are stored, which makes that syntax a lot worse.

The other issue with Goroutines is that they require the framework to handle every little thing someone could ever want to do. Intel ships an accelerator called DLB, dynamic load balancer, on all new Xeons. It provides hardware offload for a lot of the scheduling in an actor system, and gets rid of most of the data movement costs. Intel is not the only company with something like this. It means that the stdlib framework needs to handle this, and every async API under the sun, and have ways to work well with popular kernel bypass tools like DPDK and OpenOnload. Oh, and all of it needs to work on GPUs, or be able to handle running some tasks on ARM cores on a DPU (fancy network card) while others run on x86 cores on the host as others run on the RISC-V management core of an AI Accelerator.

My opinion is that the problem is too unsolved to be provided in a standard library. Yes, showing it to the user brings a lot of complexity, but there are two kinds of complexity. Incidental complexity is something like the classical description of Haskell’s Monad, which is “a monad is a monoid in the category of endofunctors”, better described as a generalization of try/catch to be able to handle more things than just errors, it’s when things are more complex than they need to be. Inherit complexity is “wow, this is a hard problem with lots of tradeoffs”. Attempting to remove inherent complexity is perilous, because it often leads to your solution being insufficient for some people, and if you don’t leave enough escape hatches in your solution, they have to throw out everything. The continued work on greenthreads, async/await, communicating sequential processes, message passing, etc, means that Computer Science, as a field, does not have a very solid grasp on how to handle this in the general case. We have a wide array of tradeoffs, and without a magic solution, languages have to choose. Go choose to make itself unusable for some types of programs in order to make many easier to write. This is fine for a lot of cases, but leads to situations like I had with my PhD advisor, someone who has written most of his projects in Go for years, where we had to totally rewrite a project because Go left us nowhere to go to solve some performance issues. Mojo is a systems language, intended for work in places where people do care about fine-grained memory management, where they need all the performance the hardware can give them, and where people are more likely to reach for exotic solutions like hardware accelerators to solve performance problems. This is a place where I don’t think we can afford to not surface all of the inherent complexity and let people choose.

We will do our best to not surface much incidental complexity, and we can offer a “probably works for you” solution, but we need to build with the idea that some people will need to take off the training wheels and get their hands dirty. If we build for those people first, then we can ensure their exacting needs are met, and then people who don’t really care can have what we think is a good idea to build on top. If someone doesn’t like the “easy mode”, then they can come down with the rest of us and build what they want.

We also have the benefit of hindsight. The stdlib will have IO traits that are async ready, and in fact the current discussion is towards having async first and leaving a way to make everything sync if you don’t want to deal with it. Tokio made several mistakes that made Rust’s async much more difficult than it needed to be, and some of Rust’s language design (the leak-pocalypse) has caused more ergonomic designs to be unsound. We do have a path to fixing that, as well as making function coloring much less of a problem for people who aren’t writing libraries (under my proposal they still need to write async correct code, but it can be made sync). We can also establish an interface for all but the most specialized executors to conform to, which should enable much better ecosystem compatibility than Rust has.

Green threads are an abstraction that I don’t think Mojo can offer cleanly without unacceptable performance sacrifices, so we’ll have something more like async/await, which can be made zero-cost performance wise and is typically more memory efficient. As for stdlib or external library, I think that partially depends on how specific your needs are. If I had full control, the stdlib will be an async runtime for “I just need to do some async IO”, and if you care about performance you go elsewhere for something that better fits your needs.

2 Likes

Thank you @owenhilyard for your thoughtful answer.

I have some food for thought for the Go community. The only restrictions you have in open source programming is proprietary software (and getting lambasted for why your idea may not be cool or a nod of approval of why it’s really cool lol). If you would like garbage collection for Mojo, it’s possible, but you may have to build it. Python has garbage collection as a possible plugin or port to Mojo. As far as runtimes, ironically I have been bouncing that idea around for my Mojo project.

This is a large reason why I support the open source community. Software is very malleable or rigid depending on its by design framework.

Stop sweet-talking me into cool projects, @rcmpge :stuck_out_tongue:

This topic was automatically closed 7 days after the last reply. New replies are no longer allowed.