[HN Gopher] Async2 - The .NET Runtime Async experiment concludes
       ___________________________________________________________________
        
       Async2 - The .NET Runtime Async experiment concludes
        
       Author : Nelkins
       Score  : 105 points
       Date   : 2024-08-22 11:52 UTC (5 hours ago)
        
 (HTM) web link (steven-giesel.com)
 (TXT) w3m dump (steven-giesel.com)
        
       | xeromal wrote:
       | It's a bummer green threads didn't work out
        
       | giancarlostoro wrote:
       | I still remember first hearing about Rust and how it was using
       | Green Threads, then out of the blue a year later passes or
       | months, and I'm reading that it doesn't do any of that anymore,
       | it's basically C++ on steroids mixed with functional programming.
       | I never did look up why they gave up on Green threads and other
       | things, I wonder if they faced similar challenges?
        
         | wyager wrote:
         | Rust has settled into an excellent design niche where the
         | language does not provide any features that require a language
         | runtime.
         | 
         | Rust's async/await is a purely compile-time feature, with no
         | runtime support required.
         | 
         | This is extremely powerful because it means you can do things
         | like easily run async code on embedded devices with no OS.
         | 
         | To run async code in rust, you bring your own executor. The
         | most popular one is tokio (for desktop OSs), but you also have
         | stuff like pollster (for running async code as blocking code)
         | and embassy or RTIC (for embedded).
        
           | jtrueb wrote:
           | Agreed, as much as there are complaints about usability of
           | async in Rust, many alternatives proposed would fail in
           | resource constrained systems. Green threading would be
           | impossible or very limiting on embedded, where each stack
           | eats away at something like 1-2% of memory and allocation is
           | likely impossible or prohibitively expensive.
           | 
           | I do note that some of the futures are starting to take up a
           | couple KiB of RAM and ROM.
        
           | eknkc wrote:
           | I might be completely wrong as I only played with rust but
           | the current method seems to couple any async libraries you'd
           | like to write to the executor. Which feels wrong imo.
           | 
           | Would it be feasible to provide async io (file, network) code
           | in std and let the executors execute?
           | 
           | Or even some std traits that abstract the filesystem, network
           | etc so that the executors can bring their own implementations
           | but at least library code could only depend on std?
        
             | wyager wrote:
             | Correct, libraries do have to either pick a specific async
             | IO library or use a trait for async IO.
             | 
             | I have seen some of the latter in embedded-land, although
             | I'm not sure if it's used in desktop async rust as well.
             | 
             | The rust compiler currently discourages async traits with a
             | warning, which seems wrong to me, but we use them
             | extensively in embedded anyway.
        
               | anonymoushn wrote:
               | The feature is less than 1 year old, so a lot of the
               | ecosystem couldn't use the feature because of being
               | written before the feature existed. It does seem silly to
               | have a warning today.
        
           | anonymoushn wrote:
           | This is a fine approach but it's trivial to ship green
           | threads with no runtime support as well. Various libraries
           | implementing the feature exist for C or Zig etc.
        
         | dboreham wrote:
         | User level (aka green) threading is difficult. Very few
         | implementations work (Erlang, Golang, <any others>+) and it
         | takes a great deal of time and effort to get to that "fully
         | working" state.
         | 
         | +Perhaps the new Java fiber stuff works but I don't have enough
         | data to be sure yet.
        
         | Ygg2 wrote:
         | There were plans for green threads, however, green threads were
         | abandoned due to runtime costs around like 6 years ago?
         | 
         | https://github.com/rust-lang/rfcs/blob/master/text/0230-remo...
         | 
         | EDIT: 10 years ago.
        
           | steveklabnik wrote:
           | It's coming up on ten years. Time flies.
        
             | Ygg2 wrote:
             | I thought it was 2018, it was 2014 instead.
        
           | anonymoushn wrote:
           | "green threads" as discussed in TFA don't require a runtime.
           | The relevant issues are maybe https://github.com/rust-
           | lang/rust/issues/33368 and https://internals.rust-
           | lang.org/t/what-is-the-current-safety...
        
         | steveklabnik wrote:
         | https://github.com/rust-lang/rfcs/blob/master/text/0230-remo...
         | 
         | Note this was in 2014, before Rust 1.0.
        
           | hyutmyjahsd wrote:
           | I miss peak rust.
        
           | giancarlostoro wrote:
           | Yes it was a while back, wow 10 years.
        
         | jasode wrote:
         | _> I still remember first hearing about Rust and how it was
         | using Green Threads, [...] I never did look up why they gave up
         | on Green threads_
         | 
         | It's because green threads have unavoidable performance costs.
         | I collected various threads that has Rust contributors
         | explaining the costs they didn't want to pay:
         | https://news.ycombinator.com/item?id=39062953
         | 
         | A language like Go is willing to pay those performance costs
         | because they deliberately sit at a higher level of abstraction
         | than lower-level Rust.
         | 
         | Each chose different tradeoffs for different goals:
         | 
         | - Golang : green threads worth the perf cost to gain higher
         | productivity (goroutines, channels, etc) for the type of
         | programs that Go programmers like to write
         | 
         | - Rust : green threads are not worth the perf cost so as to not
         | sacrifice the best possible performance for the type of
         | programs Rust programmers like to write
         | 
         | You can't have a language+runtime that satisfies both camps
         | 100% perfectly. The language designer _must choose the
         | tradeoff_. But because this tradeoff decision is not blatantly
         | well-known and disseminated ... it also perpetually confuses
         | Language X proponents on why Language Y didn 't do the same
         | thing as Language X.
        
           | 01HNNWZ0MV43FF wrote:
           | What do Go channels do that takes advantage of their green
           | threads?
        
           | giancarlostoro wrote:
           | Thank you for this! This is exactly why I posted my comment,
           | I was hoping someone would share some insight about Rust.
        
         | zamalek wrote:
         | Rust can have green threads. It depends on the reactor. The
         | abstraction sits one level lower than nearly everything else
         | out there.
        
       | cube2222 wrote:
       | This all makes sense but
       | 
       | > Green threads are different. The memory of a green thread is
       | allocated on the heap. But all of this comes with a cost: As they
       | aren't managed by the OS, they can't take advantage of multiple
       | cores inherently. But for I/O-bound operations, they are a good
       | fit.
       | 
       | this is clearly not true? Am I missing some nuance here, as I'm
       | sure the author knows what they're talking about?
       | 
       | Green threads can totally use a multi-threaded runtime, like e.g.
       | Go does, and it works just fine. The main hurdle with them is
       | arguably FFI.
        
         | DougBTX wrote:
         | The "inherently" means not by default, i.e., the runtime has to
         | support moving green threads between OS threads itself.
        
           | cube2222 wrote:
           | Ah, that makes sense, thanks!
        
           | 01HNNWZ0MV43FF wrote:
           | That's not how I use "inherent", maybe they should just say
           | "default" then?
           | 
           | But that's like... C is also single threaded by default, what
           | isn't?
        
             | jayd16 wrote:
             | They will never be transparently/fundamentalally managed by
             | the OS alone. The runtime will need to determine how to
             | juggle green threads across multiple OS threads. In that
             | way, this mapping is not inherent.
             | 
             | It can be designed around but that itself is a runtime
             | design decision and I would not say it's akin to default vs
             | custom.
        
             | recursive wrote:
             | With respect, it's not particularly relevant how you use
             | "inherent". It's a standard usage. Rather than asking the
             | whole rest of the world to change, you should probably
             | learn the definition.
        
               | layer8 wrote:
               | "Inherently" means "intrinsically", meaning it's a
               | characteristic that can't be changed without changing the
               | nature of the thing. It doesn't mean "by default".
        
         | louthy wrote:
         | Presumably, it just means there needs to be _explicit_ forking
         | of the green thread for cpu bound operations, otherwise
         | everything will run synchronously (because there's no point
         | where the green thread is paused to wait for an IO IRQ).
         | 
         | That is unless your compiler or JIT injects occasional yields
         | into your synchronous code!
        
           | Rohansi wrote:
           | And that wouldn't be great for performance.
        
             | 01HNNWZ0MV43FF wrote:
             | The overhead for epoch stopping like wasm uses can be
             | something like 1%. I did a synthetic test with native code
             | once because I was curious.
             | 
             | I think Go also injects yields into its generated code for
             | go routine scheduling
        
           | adgjlsfhk1 wrote:
           | Jits already have to do this for GC so it's actually free
        
         | bilekas wrote:
         | There are nuances with multi-threading in C#.
         | 
         | I don't agree with OP about I/O-bound ops, I think if you're
         | looking to green threading, you've taken a wrong approach.
         | 
         | > [0] the Task.Runmethod offloads the provided action to the
         | thread pool, and the await keyword yields control back to the
         | caller until the task completes.
         | 
         | All async code must be in an async call stack, virtual threads
         | are 100% transparent because its the runtime scheduling them so
         | you get a but more control than relying on the yeild of dotnet
         | at least as I see it.
         | 
         | Again I don't see the huge demand for it personally, but I
         | barely touch dotnet too often so take this with a grain of
         | salt.
         | 
         | [0] https://stackify.com/c-threading-and-multithreading-a-
         | guide-...
        
           | arghwhat wrote:
           | > I don't agree with OP about I/O-bound ops, I think if
           | you're looking to green threading, you've taken a wrong
           | 
           | It depends in the implementation. In Go for example, all I/O
           | is async and suspend your green thread, replacing it with
           | another runnable green thread.
           | 
           | This works the same as if you managed an event loop on your
           | own for the purpose of I/O, which is the best way to handle
           | I/O outside for regular user space code. It's just automatic
           | with your code resembling a simple, blocking scenario.
           | 
           | OPs note on threading would be C# or runtime specific - green
           | threads have no problem with parallelism, with runtimes
           | commonly having a thread per core (or more) and having them
           | all run green threads in parallel.
        
         | neonsunset wrote:
         | What this likely means is for you to take advantage of the
         | underlying runtime multiplexing green threads over multiple
         | physical ones running on multiple cores, you need to explicitly
         | fork the execution flow.
         | 
         | This could be as simple as a web server firing off a new green
         | thread or a goroutine for an incoming request, or as contrived
         | as doing so manually within a function scope.
         | 
         | In practice, there really is not much difference with
         | async/await. "Green threads" is a combination of implementation
         | details and a subset of what async/await abstractions achieve.
         | 
         | Effectively, Goroutines are in many ways similar to C#
         | Task<T>s. The difference is that in Go you are expected to
         | explicitly send the result via a channel or some other data
         | structure and then synchronize the completion of the execution,
         | where-as with tasks you simply await that.
         | 
         | There could be an argument made about preference of implicit
         | suspend (Go, Java, BEAM family) over explicit suspend (C#/F#,
         | Rust, JS, Python, C++ co_await, Swift), but for practical
         | purposes invoking a function with 'go' keyword in Golang is
         | very similar to firing off a synchronous method with Task.Run
         | in C#, or calling an asynchronous method (with sufficiently
         | short body before first yield) and _not_ immediately awaiting
         | it.
         | 
         | As I usually post it on HN, tasks make the following patterns
         | trivial:                   using var http = new HttpClient {
         | BaseAddress = new("https://news.ycombinator.com/")         };
         | // not immediately awaited requests are executed in parallel
         | var frontPage = http.GetStringAsync("news?p=1");         var
         | secondPage = http.GetStringAsync("news?p=2");
         | Console.WriteLine($"{await frontPage}\n\n{await secondPage}");
        
           | spinningslate wrote:
           | > The difference is that in Go you are expected to explicitly
           | send the result via a channel or some other data structure
           | and then synchronize the completion of the execution, where-
           | as with tasks you simply await that.
           | 
           | That may be be the case in Go but it's not an inherent
           | property of green threads. See, for example, Gleam Tasks [0]
           | which are based on green threads and provide the syntatic
           | convenience of being able to await the result rather than
           | receiving a message:                   let task =
           | task.async(fn() { do_some_work() })         let value =
           | do_some_other_work()         value + task.await(task, 100)
           | 
           | They do so without the disadvantage of bifurcating the code
           | base into sync and async functions.
           | 
           | [0] https://hexdocs.pm/gleam_otp/gleam/otp/task.html
        
             | neonsunset wrote:
             | Of course.
             | 
             | The discussion regarding Goroutines is to highlight that,
             | despite prevalent claims of otherwise, they are not doing
             | something unique and for developers who are used to
             | languages with powerful concurrency primitives look like an
             | incomplete task abstraction. "Green Threads" really is an
             | implementation detail, in many ways orthogonal to pros/cons
             | of implicit and explicit suspend points.
             | 
             | I hope your opinion about C#'s task system has improved
             | since the last time[0], given what Gleam (and, in many
             | ways, Elixir) does looks practically identical :)
             | 
             | [0]: https://news.ycombinator.com/item?id=40427935
        
         | pron wrote:
         | The efficiency and complexity of user mode threads heavily
         | depend on constraints imposed by the particular language. E.g.
         | if the language supports pointers into the stack, user mode
         | threads would be less efficient; if the language is largely
         | dependent on manual memory management -- user mode threads
         | would be more expensive; if the language already has some other
         | concurrency primitives (like async/await) -- user mode threads
         | will be more expensive (although in this case in terms of
         | complexity rather than runtime efficiency). Because Java
         | exposes relatively little of its implementation details, we've
         | been able to implement efficient user mode threads even without
         | any FFI overhead.
        
       | bob1029 wrote:
       | I don't know if this proposal makes a lot of sense.
       | 
       | The existing async1/TPL path is stable & predictable. If you find
       | yourself needing more performance, you can reach for hardware
       | Thread instances and use whatever locking/waiting/sharing/context
       | strategies you desire. Anything else is a weird blend of
       | compromises that is going to have caveats that are not
       | immediately obvious for your specific use case.
       | 
       | For example, async2 w/ runtime JIT appears to have some tradeoffs
       | with regard to GC & memory usage and the experiment writeup
       | leaves some open-ended questions here[0].
       | 
       | [0]:
       | https://github.com/dotnet/runtimelab/blob/feature/async2-exp...
        
         | whaleofatw2022 wrote:
         | Trying to do clever stuff with threads tends to lead from brow
         | beating, wailing, and gnashing of teeth by various folks who
         | insist to just trust the threadpool.
        
         | GordonS wrote:
         | > If you find yourself needing more performance, you can reach
         | for hardware Thread instances
         | 
         | True for code your team write, but async/await is kind of
         | viral, and many libraries now only have async APIs, which makes
         | them difficult to shoehorn into a threading approach.
        
           | bob1029 wrote:
           | > async/await is kind of viral
           | 
           | Exactly part of my concern. Hypothetically we've got 2
           | competing viruses now.
        
       | neonsunset wrote:
       | For everyone reading this blog post I caution to take the
       | conclusions there with a grain of salt as they are an
       | interpretation of the notes written down here:
       | https://github.com/dotnet/runtimelab/blob/feature/async2-exp...
       | 
       | It is difficult to draw conclusions at the present moment on e.g.
       | memory consumption until the work on this, which is underway,
       | makes it into mainline runtime. It's important to understand that
       | the experiment was first and foremost a research to look into
       | modernizing async implementation, and was a massive success. Now
       | once that is proven, the tuned and polished implementation will
       | be made.
       | 
       | Once it is done and makes into a release (it could even be as
       | early as .NET 10), then further review will be possible.
       | 
       | With that said, thank you for writing it, .NET tends to be
       | criminally underrated and slept on by the most of the industry,
       | and these help to alleviate it even if just a bit.
        
         | hirvi74 wrote:
         | > .NET tends to be criminally underrated and slept on by the
         | most of the industry
         | 
         | I have been programming in .Net/C# for about 8 years now. I
         | absolutely hate Microsoft with every fiber of my being.
         | However, I can't thank them enough for C# (and all the FOSS
         | contributors as well). .Net has been such a pleasant experience
         | that I truly do not want to program in any other language.
        
           | naasking wrote:
           | > I absolutely hate Microsoft with every fiber of my being.
           | 
           | Why? I get it back when they were super hostile to Linux and
           | open source, but those days are long past.
        
             | anonymoushn wrote:
             | Maybe because GP uses Windows sometimes.
        
             | orphea wrote:
             | They are super hostile to users of their OS, and these days
             | are right now.
        
               | geodel wrote:
               | Absolutely. It is just sad experience using Windows
               | beyond some web browsing with Non-MS browser.
        
             | MangoCoffee wrote:
             | >hostile to Linux and open source, but those days are long
             | past
             | 
             | Microsoft remains a giant elephant with questionable
             | business practices. Their shift to SaaS and cloud computing
             | meant they have to play nice with others.
        
         | runevault wrote:
         | I see people complain sometimes that c# adds too many features,
         | and I understand the concern, but with limited exceptions (I
         | don't like the new global using stuff), they tend to feel
         | tasteful to me. Also I'm really hopeful we'll get Discriminated
         | Unions (under a name I'm forgetting) in c# 10.
        
       | ikekkdcjkfke wrote:
       | What is the fundemental problem we are trying to solve here? My
       | pet problem with async is that it takes so much syntax, and i
       | wonder why we need to do that. As i understand it it is all about
       | blocking calls taking up OS threads.
       | 
       | So i will try to naievly solve it here, and maybe i end up with
       | the same conclusions.
       | 
       | When doing a blocking call, the OS thread could just start
       | executing something else transparently and not 'yield' anything
       | to the code, it just stops executing the code and comes back and
       | executes further, no await, no tasks no nothing. Ok, but how did
       | we end up in a thread? The http listener started 4 threads and
       | it's just putting a stack of function pointers and context memory
       | for the threads to eat when they want. There is a separate engine
       | on another OS thread that handles where the threads can start
       | executing ready code again. No Task or await keywords show up in
       | this code. I have no idea how stack traces work but i guess that
       | can just be saved to memory and loaded back again when the
       | threads feel like executing some ready code
        
         | irdc wrote:
         | The rationale for async/await I keep hearing is that the
         | separate stack required for each thread uses a lot of memory.
         | I'm not sure going full on function colouring is the answer
         | though. A concerted effort for language runtimes to use less
         | stack space and then simply allocating smaller thread stacks
         | sounds to me like a more elegant solution. It certainly is a
         | whole lot easier to debug than a deeply-nested async/await
         | chain.
        
           | iknowstuff wrote:
           | But also avoiding the cost of context switching, and the
           | ability to handle many tasks in embedded environments.
           | 
           | https://tweedegolf.nl/en/blog/65/async-rust-vs-rtos-showdown
        
         | layer8 wrote:
         | What you propose is similar to how Java virtual threads work.
        
         | jayd16 wrote:
         | Ok well OS threads are already scheduled this way... The
         | drawback is they're heavier because they make assumptions that
         | the runtime doesn't need to make.
         | 
         | Furthermore, explicit Async/await provides a syntax for co-
         | opoerative multithreading and that enables other patterns that
         | implicit designs don't.
        
       | kodablah wrote:
       | > And for the transition phase, there has to be interop for async
       | - async2
       | 
       | For those like me who weren't clear whether `async2` was expected
       | to be a real keyword in the final language, it's not[0].
       | 
       | 0 -
       | https://github.com/dotnet/runtime/issues/94620#issuecomment-...
        
         | augusto-moura wrote:
         | async2 looks like such a terrible keyword name, that it didn't
         | even crossed my mind as an option. Only following your linked
         | comment I understood that they used it as a temporary name
         | while testing
        
       ___________________________________________________________________
       (page generated 2024-08-22 17:01 UTC)