Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Green Thread Experiment Results #2398

Open
davidwrighton opened this issue Sep 8, 2023 · 50 comments
Open

Green Thread Experiment Results #2398

davidwrighton opened this issue Sep 8, 2023 · 50 comments
Labels
area-green-threads Green threads

Comments

@davidwrighton
Copy link
Member

davidwrighton commented Sep 8, 2023

Our goal with the green thread experiment was to understand the basic costs and benefits of introducing green threads to the .NET Runtime environment.

Why green threads

The .NET asynchronous programming model makes it a breeze to write application asynchronous code, which is crucial to achieve scalability of I/O bound scenarios.

I/O bound code spends most of its time waiting, for example waiting for data to be returned from another service over the network. The scalability benefits of asynchronous code come from reducing the cost of requests waiting for I/O by several orders of magnitude . For reference, the baseline cost of a request that is waiting for I/O operation to complete is in the 100 bytes range for async C# code. The cost of the same request is in the 10 kilobytes range with synchronous code since the entire operating system thread is blocked. Async C# code allows a server of the given size to handle several orders of magnitude more requests in parallel.

The downside of async C# code is in that developers must decide which methods need to be async. It is not viable to simply make all methods in the program async. Async methods have lower performance, limitations on the type of operations that they can perform and async methods can only be called from other async methods . It makes the programming model complicated. What color is your function is a great description of this problem.

The key benefit of green threads is that it makes function colors disappear and simplifies the programming model. The green threads should be cheap enough to allow all code to be written as synchronous, without giving up on scalability and performance. Green threads have been proven to be a viable model in other programming environments. We wanted to see if it is viable with C# given the existence of async/await and the need to coexist with that model.

What we have done

As part of this experiment, we prototyped green threads implementation within the .NET runtime exposed by new APIs to schedule/yield green thread based tasks. We also updated sockets and ASP.NET Core to use the new API to validate a basic webapi scenario end-to-end.

The prototype proved that implementing green threads in .NET and ASP.NET Core would be viable.

Async style:

var response = context.Response;
response.StatusCode = 200;
response.ContentType = "text/plain";
response.ContentLength = payload.Length;
// Call async method for I/O explicitly
return response.Body.WriteAsync(payload).AsTask();

Green thread style:

var response = context.Response;
response.StatusCode = 200;
response.ContentType = "text/plain";
response.ContentLength = payload.Length;
// Call sync method. It does async I/O under the covers! 
response.Body.Write(payload);

The performance of the green threads prototype was competitive with the current async/await.

ASP.NET Plaintext Async Green threads
Requests per second 178,620 162,019

The exact performance found in the prototype was not as fast as with async, but it is considered likely that optimization work can make the gap smaller. The microbenchmark suite created as part of the prototype highlighted the areas with performance issues that future optimizations need to focus on. In particular, the microbenchmarks showed that deep green thread stacks have worse performance compared to deep async await chains.

A clear path towards debugging/diagnostics experience was seen, but not implemented.

Technical details can be found in https://github.com/dotnet/runtimelab/blob/feature/green-threads/docs/design/features/greenthreads.md

Key Challenges

Green threads introduce a completely new async programming model. The interaction between green threads and the existing async model is quite complex for .NET developers. For example, invoking async methods from green thread code requires a sync-over-async code pattern that is a very poor choice if the code is executed on a regular thread.

Interop with native code in green threads model is complex and comparatively slow . With a benchmark of a minimal P/Invoke, the cost of making 100,000,000 P/Invoke calls changed from 300ms to about 1800ms when running on a green thread. This was expected as similar issues impact other languages implementing green threads. We found that there are surprising functional issues in interactions with code which uses thread-local static variables or exposes native thread state.
Interactions with security mitigations such as shadow stacks intended to protect against return-oriented programming would be quite challenging.

It is possible or even likely that we could make the green threads model (a bit) faster than async in important scenarios. The key challenge is that this capability would come with a cost of it being significantly slower in other scenarios and having to give up compatibility and other characteristics.

It is less clear that we could make green threads faster than async if we put significant effort into improving async.

Conclusions and next steps

We have chosen to place the green threads experiment on hold and instead keep improving the existing (async/await) model for developing asynchronous code in .NET. This decision is primarily due to concerns about introducing a new programming model. We can likely provide more value to our users by improving the async model we already have. We will continue to monitor industry trends in this field.

@jkotas jkotas added the area-green-threads Green threads label Sep 9, 2023
@rogeralsing
Copy link

Interop with native code in green threads model is complex and comparatively slow . With a benchmark of a minimal P/Invoke, the cost of making 100,000,000 P/Invoke calls changed from 300ms to about 1800ms when running on a green thread.

It would be super interesting to hear why and why the overhead is so significant.
Do we have some comparisons to other languages with green threads, is it as significant there too?

@bitbonk
Copy link

bitbonk commented Sep 9, 2023

What kind of improvements of the existing (async/await) model do you have in mind?

@yugabe
Copy link

yugabe commented Sep 9, 2023

Sounds great, and I believe this to be the right decision based on the outcomes and results described. Maybe solving the problem some other way would be more efficient, by not making drastic changes to how the current coding model is and turning everything upside down. If there was a way to correctly, safely and performantly "await" async calls in sync code (if you could use await in sync code essentially), not having green threads wouldn't even pose as big a problem. Or even if a method itself wasn't being sync or async based on its signature alone, but rather its usage and analyzed control flow.

@ladeak
Copy link

ladeak commented Sep 9, 2023

I have seen numerous existing, LOB applications (being developed for over a longer period), that do still do many-man sync IO - that nobody can reasonably update to async/await as the whole stack needs to become async/await too. Typically, there are thousands of sync EF/EFCore DB requests implemented. Having a switch to enable green threads to get a significant perf gain sounds very appealing for these scenarios.

@StefanKoell
Copy link

What about other scenarios? I understand that for a web server the conclusion might be spot on but if you are coding UI apps (like WinForms) and heavily rely on consuming event handlers, green threads might be a better solution. Async/await is great for specific use cases but horrible for other use cases. Having an alternative (even when a bit slower) would be very welcome.

@maxkatz6
Copy link

maxkatz6 commented Sep 9, 2023

@StefanKoell can you give an example where green threads are better for GUI apps?

From my experience, event-like nature of async/await works pretty good in GUI apps. And current sync context based infrastructure also works well.

The only possibly disadvantage is sync event handlers. But "async void" there behaves as expected, where any exception is not lost, but raised on the dispatcher (UI thread jobs queue).

@StefanKoell
Copy link

@maxkatz6 maybe I'm missing something but having async void methods (event handlers) will swallow exceptions and are hard to handle in general.

The only possibly disadvantage is sync event handlers. But "async void" there behaves as expected, where any exception is not lost, but raised on the dispatcher (UI thread jobs queue).

Not sure I understand that. Can you elaborate?

Another really pain is to work on brown-field projects and try to implement new functionality using async/await. It's so painful because it's a rabbit hole where you have to rewrite every single funtion in the call chain to make it work. And sometimes, you just can't do it properly because of some 3rd party dependency and you are forced to use GetAwaiter().GetResult - which could cause deadlocks.

The same is true for events which have CancelEventArgs. There's no elegant solution. You have to implement ManualResetEvents, so the code gets really complicated.

If I need functionality which runs sync and async I basically have to write it twice. There's no easy way to just tell a sync method to run async or vice versa (without the hurdles of GetAwaiter().GetResult0.

I was hoping that Green Threads are universal and could simply decide whether to run a method sync or async without all the pain and gotchas mentioned above. That would have been nice and suitable for many scenarios - especially in large brown-field projects.

@SmartmanApps
Copy link

The downside of async C# code is in that developers must decide which methods need to be async.

Affirmative on that one, and so...

Call sync method. It does async I/O under the covers!

...this sounds really good!

I'm currently wanting to implement an interface which will work with either the native (i.e. .NET) filesystem, or use a 3rd party (e.g. Dropbox). Currently for creating a folder .NET ONLY provides a sync method, and Dropbox ONLY provides async methods (sigh). It would be great to not have to worry about that.

Anyone reading this who knows, I'm currently looking for the best pattern to use to implement this. i.e. the method may or may not be async depending on which provider is currently in use.

@Scooletz
Copy link

Scooletz commented Sep 9, 2023

I cannot express how massive this work looks. Being given a lot of effort put into asyncification of projects out there, are you planning to lay out some migration map as well? Or it much to early to have this discussions? There is a lot of projects that spent a lot of time on making their code bases async-friendly when Tasks arrived. Having some good guidance would be helpful.

@Eirenarch
Copy link

What about other scenarios? I understand that for a web server the conclusion might be spot on but if you are coding UI apps (like WinForms) and heavily rely on consuming event handlers, green threads might be a better solution. Async/await is great for specific use cases but horrible for other use cases. Having an alternative (even when a bit slower) would be very welcome.

I'd expect that desktop apps are much more likely to have use cases where they do native interop and therefore green threads are likely more problematic for them than for ASP.NET apps

@StefanKoell
Copy link

@Eirenarch maybe I'm missing something but why exactly would that be a problem? I haven't really seen any sample code how green threads are used exactly and how the programming model looks like. What would be problematic?

@Eirenarch
Copy link

@Eirenarch maybe I'm missing something but why exactly would that be a problem? I haven't really seen any sample code how green threads are used exactly and how the programming model looks like. What would be problematic?

IIRC (don't quote me on that) when using green threads the runtime constantly plays with the (green)thread stack which means that calling native code which is not aware of these swaps will fail. To work around this the runtimes which use green threads do a bunch of marshalling which kills the performance of native code calls.

@riesvriend
Copy link

Great that you did this experiment and mapped a path forward. Too bad it’s on hold however. Because

Line of business apps and domain logic code greatly benefit from having a lower tier concern such as IO hidden from the code. Its now as if c# is moving towards C and away from its VB roots.

Asynch is very verbose in the code and gets most attention, where as the coder/user wants the OOP model to be the focus and tied to business entities, not IO concepts or infrastructure

Eg “total = order.Lines().Sum(line => line.Amount), and not “total = await (order.Lines()).Sum(l => l.Amount).

it’s not for all cases, like a web server module, but for classic VB style readability in business workflow apps green threads would as much a win as hot update support in C# was in continuation of VB6 debug-edit-and-continue. So I hope you follow the edit and continue roots.

@rcollette
Copy link

rcollette commented Sep 9, 2023

I never really understood why async Task<T> couldn't be inferred when using await in a method. ex.

public string DoSomethingAsync(){
    string x = await getSomeValueFromTheDB();
    // Do more work
    return x;
}

Why can't the compiler infer that the method is actually returning async Task<string> in this case? Frequent non-business logic typing would be eliminated in this case.

And for that matter, why can't it infer, when I am assigning a Task return value to a non Task variable, that I want to use await in that case? You would have a compiler error anyway so you're forced to await and in a forced situation, can't the compiler make an inference? For the less common case where you need to do Task.AwaitAll or similar then you would assign return values to Task<T> variables and you wouldn't need to use the async keyword in this case either.

Make the behavior a compiler switch for those that prefer their code to be explicit.

@CyrusNajmabadi
Copy link
Member

I never really understood why async Task couldn't be inferred

It could be. The question is: is that good thing?
For example, if you do that silently, give now made a trivial binary breaking change without the user ever being aware of it.

For that reason, we prefer the abi to be explicit. We could have gone a different direction, but we believe it's better this way.

@CyrusNajmabadi
Copy link
Member

CyrusNajmabadi commented Sep 9, 2023

Why can't the compiler infer that the method is actually returning async Task in this case?

Because that might be very wrong and undesirable. Many other task-like types might be more appropriate there.

It's important to understand that Task/Task<> are not special from the perspective of the language. They're just one of many possibilities in an open ended set. We don't want the language making such opinionated choices unless there really is only one true choice that is going to be right all the time. Especially not for something as important and flexible as asynchrony

@CyrusNajmabadi
Copy link
Member

And for that matter, why can't it infer, when I am assigning a Task return value to a non Task variable, that I want to use await in that case?

For the same reason as above. This would actually be hugely bad for at least one large group of customers depending on how we did this. Either we would do the equivalent of a naked-await, and be bad for libraries. Or we'd do the equivalent of ConfigureAwait(false) and be terrible for things that depend on sync contexts.

Different domains need different patterns, which is why this is an intentionally explicit system.

@CyrusNajmabadi
Copy link
Member

Make the behavior a compiler switch for those that prefer their code to be explicit.

We are strongly opposed to dialects of the language on principle. We've only added one in the last 25 years, and only because the benefit was so overwhelming, and the costs too high for the ecosystem otherwise for the success of that feature.

That doesn't apply here, so it would be highly unlikely for us to go that route and bifurcate the ecosystem.

@rcollette
Copy link

From my perspective, things like minimal APIs and top level statements bifurcate the system, but it seems like there was a justification for doing so, ease of adoption I believe being one of them, and ease of use is something myself and others find relevant.

There are people/systems that have different goals. Writing a highly engineered performant library that is going to be used for video processing, or at a scale like Netflix, Amazon and others, those warrant utilization of very explicit Task types (and Spans, Vectors, etc.), and it's fantastic that .NET provides such great performance in these scenarios. For a large number of people, there is a reduced level of performance scrutiny required, as they balance out their workload with the need to produce more business logic. "Wrong/Right" is a matter of perspective and situation.

Like others, I've found myself in a situation where something that I never would have thought would be async (I don't know, setting a message header or JSON serialization) suddenly became an async operation and next thing you know you're in a rabbit hole of updates. Having the compiler handle that, when its able to, is a win, at least for some of us, even if it's not perfect from an engineering perspective, because it gets you to a better place and you can always go back and be more explicit if need be. Premature optimization can be a fault.

@CyrusNajmabadi
Copy link
Member

CyrusNajmabadi commented Sep 9, 2023

For a large number of people, there is a reduced level of performance scrutiny required,

If performance isn't a concern then just keep everything synchronous. For any async-only apis, just FYI sync-over-async. It's not good, but it's not going to matter if you're on a reduced perf scrutiny environment. :-)

Like others, I've found myself in a situation where something that I never would have thought would be async (I don't know, setting a message header or JSON serialization)

Premature optimization can be a fault.

In this case, you didn't have to go full async. Esp if you are not in a high perf scenario (like you mentioned). Just do the simple thing here and you'll get a solution that works with minimal fuss or perf impact. Likely far less impact than if you had to switch to Green threads. :-)

@CyrusNajmabadi
Copy link
Member

Wrong/Right" is a matter of perspective and situation.

Sure. I'm just explaining the perspective as one of the language designers. That there are many of these groups, and we don't want to bifurcate over them, led to the decisions I was commenting on.

@CyrusNajmabadi
Copy link
Member

and top level statements bifurcate the system,

They don't bifurcate the language (which is what I was responding to when I was discussing compiler switches). Top level statements are just c#. They're not an alternative set of semantics that you need to opt into with a compiler switch that then changes them meaning of existing code.

Having different meanings for the same code is what we mean by 'dialects' and 'bifurcation'. Having the language support more, while preserving samantic-compat with the last 25 years of c# is fine :-)

@riesvriend
Copy link

Make the behavior a compiler switch for those that prefer their code to be explicit.

We are strongly opposed to dialects of the language on principle. We've only added one in the last 25 years, and only because the benefit was so overwhelming, and the costs too high for the ecosystem otherwise for the success of that feature.

How about a new block type to auto-await code? It essentially a matter of preprocessing for the purpose of domain clarity over IO/concurrency clarity.

await { var total = order.Lines().Sum() }

@CyrusNajmabadi
Copy link
Member

How about a new block type to auto-await code?

Sure. Feel free to propose and design over at dotnet/csharplang. If the proposal picks up steam, it could definitely happen.

@mostmand
Copy link

What I realize from the discussion is that the existing async/await pattern that has been the recommended approach for i/o intensive workloads has become too much of a burden to coexist with green threads. I wish that some brand new language would come to dotnet runtime with no backward compatibility concerns.

@kant2002
Copy link
Contributor

I would like to express "dissatisfaction" with report. I honestly expect it to be more technical in nature, and not "informational" only. I understand that a lot of time passed after experiment was done, and maybe some details is faded in memory. But if possible that it was cut due to lack of time for preparing more technical explanation, I really would like to read more about what was broken/complicated. And probably what was surprisingly easy to do.

@tannergooding
Copy link
Member

I wish that some brand new language would come to dotnet runtime with no backward compatibility concerns.

.NET is itself a 20yo ecosystem, it is not just the languages, but also the runtime, the libraries, the tooling, etc. You will always have back-compat concerns and the only way to get rid of some of those concerns would be to start a new ecosystem from the ground up. That being said, starting a new ecosystem would be a massive undertaking for honestly little benefit. Sure there's things we wish were different, but none so much that they cause unmanageable or unreasonable to handle issues; and none that are so fundamental that it necessitates starting over.

I'd also point out that the developers that would most benefit from green threads are the ones that can't or won't rewrite their app to use proper async/await. Such codebases would be unlikely to get approval to move to a new ecosystem/language for similar reasons as to why the async/await rewrite is rejected. Often this is that the short term cost is "too high", even if the long term benefit justifies it.

Finally, green threads are not some magic feature that simply make everything better. They are a feature that can make some types of existing code better and which come with their own negatives/drawbacks. Namely it can improve existing purely synchronous code by making it perform closer to how properly written async/await code would, but it gives the devs less control and may not work well with scenarios that leave the "green thread aware" domain (the primary example of which is interop with native or other languages like Java/ObjC) or with scenarios that are trying to do more explicit control via explicit tasks or even explicit threading. So while it might help code that can't migrate; it could inversely hurt code that has already migrated.

-- Views/thoughts here are my own and may not be shared by everyone

@Xhanti
Copy link

Xhanti commented Sep 10, 2023

@tannergooding your points are well reasoned, but it doesn't address the can't make it async scenario. In a scenario where you can't make things async all the way(dependency you are not in control of) and you have to 'hack' it (sync over async) there is no good answer. That's always bothered me and green threads seemed an elegant solution. The issue around native interop and interoperability with other languages is a significant issue but I don't know.

It's trade offs, maintain backwards compatibility, performance of interoperability, finer control over async code vs drastically simpler developer mental model of async code.

The older I get and the more code I write the more I lean towards simpler mental models. I care so much about it I can leave some perf on the table to keep things simpler. But that's just my limited experience.

Having said that, it's your guys call which way the trade off goes and the team has presented a rational decision. I don't have to like it, but I understand and respect it.

@tannergooding
Copy link
Member

but it doesn't address the can't make it async scenario. In a scenario where you can't make things async all the way(dependency you are not in control of) and you have to 'hack' it (sync over async) there is no good answer.

Notably sync over async is the more dangerous one. Stephen covers it in this (now nearly 10yo) blog post: https://devblogs.microsoft.com/pfxteam/should-i-expose-synchronous-wrappers-for-asynchronous-methods/. But the basic premise is that trying to synchronize asynchronous code can easily lead to hangs or deadlocks and so you shouldn't do it.

The inverse, taking someone else's code and making it asynchronous, is generally safe. It just may not scale or compose as well in the grander scheme and so its better to expose explicitly async methods where possible. There is a partner blog post covering that here: https://devblogs.microsoft.com/pfxteam/should-i-expose-asynchronous-wrappers-for-synchronous-methods/

However, what this basically means is that you can make almost always make your own code asynchronous and you can choose to offload your dependencies via wrappers if they aren't asynchronous themselves. This can require a bit more work and potentially some new locks/synchronization primitives, but it is generally safe and not terribly difficult. While, your dependents can't make your code synchronous since its too dependent on the implementation. So you may need to be extra mindful if you provide a library with only an async implementation.

Green threads are basically automatic async over sync. That is, at a high level its effectively the runtime/language/etc implicitly inserting code to make your synchronous code run asynchronously where possible. You can then think of async/await almost
as "manual green threads", and you'll notably find them mentioned as such on the green thread wiki page; among other places.

The older I get and the more code I write the more I lean towards simpler mental models. I care so much about it I can leave some perf on the table to keep things simpler. But that's just my limited experience.

For sure. Keeping your code simple, readable, and maintainable is often paramount. That being said, its all in a balance based on your target audience. Some people use .NET for a simple UI or LoB app where perf doesn't matter. It can be run overnight and be ready by morning. Other people use .NET for high speed services that need to scale to support millions of concurrent connections.

Finding the right features so that .NET as a whole can support both sides is important. We'll find that balance eventually, it just takes time. It might even be interesting to see if source generators, interceptors, or analyzers can be used to help provide safe async over sync for the cases where it is required. There's a lot of options that are available and which can make the required process even where users are otherwise "blocked".

@rubenwe
Copy link

rubenwe commented Sep 10, 2023

Some people use .NET for a simple UI or LoB app where perf doesn't matter. It can be run overnight and be ready by morning.

I think this whole mentality of being fine with unreasonably slow software needs to die. Modern CPUs, Memory, Drives and Networks are absurdly fast. Using them at least somewhat efficiently, and more importantly, having a language that provides the means to do so is great.

You are hurting your own productivity, that of other folks - hell, even the environment, by wasting all those CPU cycles.

There's a trade-off at some point, for sure. Not every line of code needs to be micro-benchmarked. This mentality of performance being a non-concern for regular Joes and Janes, writing boring LoB apps, is frankly unhelpful. If it's slow and doesn't have to be, make it go faster. You have the tools. They are there to be used.

In most cases it only takes a few minutes to identify and optimize the biggest bottlenecks. Very few problems need to eat up multiple hours of execution.

Green Threads are not going to do anything for those pieces of code, wasting cycles, doing nothing.

Please correct me if I'm wrong, but I would expect the real world scenarios where they would be beneficial (for existing code) to be very limited. You would need a decent amount of blocking IO, right? To a point where Threadpool starvation becomes a thing. One would hope that folks running into these limits today can find the time to modernize the most critical paths in their applications.

With all that said, it's really nice to see these results and to know that there could have been an alternative reality where we ended up with this approach over async/await.

Given that the most compelling argument would be that functions don't need to be colored a certain way, but we already introduced async/await, it's a hard sell now - and putting it on hold is IMHO the absolute right call.

Thank you so much for the work 👍

@HaloFour
Copy link

HaloFour commented Sep 11, 2023

Thank you for conducting the experiment. It's amazing work!

The experiment doesn't really explain some of the other pitfalls with a green threads model. They don't magically fix anything. If you block a green thread you still block the carrier kernel thread beneath it, which completely defeats the purpose. You only get the "magic" if the runtime changes every possible place you can block on I/O and rewrites it to detect that it's on a green thread and to handle wiring up an asynchronous notification to resume that green thread. That is exactly what Java Loom has done, rewrote all I/O and locking APIs. Those that couldn't be rewritten have to bounce off of a shared thread pool to "emulate" non-blocking I/O. It was a massive amount of work, and there are still scenarios where you can accidentally "pin" and cause blocking, so much that the JVM has an option to log when it happens.

IMO, Loom only really works because the JVM has a nearly hostile approach to interop. They could take the gamble that nearly all I/O operations will go through the JVM's own APIs, and thus most projects will be able to take advantage of it. Any project that does use native libraries to perform I/O need to rewrite those libraries to correctly wire up that notification and park the thread. I believe that since the .NET ecosystem is so much more mixed that it would be very difficult to address the amount of blocking non-.NET code which will continue to block the underlying kernel thread, again defeating the purpose.

Lastly, that native interop introduces another wrinkle in that these green threads can ping-pong between carrier kernel threads on every I/O boundary. Libraries not written to understand this, like Win32, will only see that you're trying to do something from the wrong thread. That means that the synchronization issues that we have to deal with in async/await will remain, except now they apply to pretty much any API that could ever do I/O or blocking operations.

It's possible that I'm missing some special magic here that the experiment attempted to apply, and maybe there are more systemic ways of approaching it, but my understanding of Loom and goroutines and the like seem to indicate that these are very challenging problems to solve and require a tightly controlled ecosystem in order to avoid actual blocking.

I probably sound like I'm very anti-green-thread. I think they're an excellent solution when the ecosystem can be carefully accommodating to them. I am very much looking forward to Loom being officially "released" in about a week and anticipate porting some of my Spring WebFlux services to using it before the end of the year. I'm more hesitant to believe that the .NET ecosystem could migrate fully over to supporting them, especially with all of the third-party non-.NET code out there that would have to be refactored.

@rcollette
Copy link

rcollette commented Sep 11, 2023

This presentation about loom/Java 21 was pretty good for those looking to understand more about green threads.
https://blog.jetbrains.com/idea/2023/05/new-livestream-virtual-threads-and-structured-concurrency-in-java-2021-with-loom/

I would expect the real world scenarios where they would be beneficial (for existing code) to be very limited. You would need a decent amount of blocking IO, right? To a point where Threadpool starvation becomes a thing.

In the java world this happens quickly due to a few reasons:

  • Java doesn't have a standard non-blocking database driver specification. Java should have addressed this before they even took on green threads. It's easy to say, "you've got operations that last too long" but there are some transactions by nature that run long and are sometimes out of your control. This is is the primary issue.
  • The reactive style asynchronous patterns/api are a mental leap for some developers, and they simply try to avoid it (not saying this is right, but it happens... all the time). It's worse than Promise nesting in JS. For every method you use you have to consider am I returning the same type or not, am I returning another promise (Future), am I working with a collection or a single value, all requiring different method calls and the list goes on. You also get into thread context scenarios such as transactions, logging MDC, etc., that just seem to fail for not so obvious reason in the reactive model, again discouraging developers. Saying "they are developers, they should just make it right or get out of the business" takes all the onus off of platform developers to provide elegant solutions. It's a problem that Java has to a fault and frankly wouldn't want that to happen in .NET, because that lack of elegance in Java is why I prefer .NET.

@HaloFour
Copy link

HaloFour commented Sep 11, 2023

@rcollette

Yes, Loom (or async/await for that matter) are only really critical when you need to achieve a degree of concurrency that would become too expensive to manage with blocking threads*. 1000 operations in flight is, by default, 1 GB worth of allocated stack space in threads. Not to mention the impact that has on the OS scheduler, etc. Loom isn't perfect (especially since you can't yet manage the underlying carrier thread scheduler, it uses a shared fork/join pool), but it does instantly make it much cheaper to block threads. Up to three orders of magnitude cheaper.

* Oops, forgot to mention not block UI threads and rendering UI apps non-responsive. I guess that's a much more common use case. :D

@IS4Code
Copy link

IS4Code commented Sep 11, 2023

I have not checked the implementation in detail, but if it would enable something that I expect from traditional fibers or Lua coroutines, I am all for it! Specifically being able to spawn a coroutine and make it yield values and wait to be resumed without any overhead. This would not only simplify asynchronous patterns but iterators too!

@JustDre
Copy link

JustDre commented Sep 21, 2023

How about a new block type to auto-await code? It essentially a matter of preprocessing for the purpose of domain clarity over IO/concurrency clarity.

await { var total = order.Lines().Sum() }

I like this syntax because it seems complementary to async/await. On the other hand, it needs to "uncolor" the functions inside the block, and I'm not sure how that could be done automatically.

@riesvriend
Copy link

riesvriend commented Sep 21, 2023

await { var total = order.Lines().Sum() }
I'm not sure how that could be done automatically.

The compiler can transpile all method calls inside the block that return a Task/ValueTask to each be awaited

@vukovinski
Copy link

I'm currently wanting to implement an interface which will work with either the native (i.e. .NET) filesystem, or use a 3rd party (e.g. Dropbox). Currently for creating a folder .NET ONLY provides a sync method, and Dropbox ONLY provides async methods (sigh). It would be great to not have to worry about that.

I believe you could get around the performance issues of that by using ValueTask in the interface and sync/async methods in the implementation.

@agocke
Copy link
Member

agocke commented Sep 25, 2023

For anyone looking for more technical details, a report has been checked in with what we found: https://github.com/dotnet/runtimelab/blob/bec51070f1071d83f686be347d160ea864828ef8/docs/design/features/greenthreads.md

@shybovycha
Copy link

shybovycha commented Sep 26, 2023

Correct me if I'm wrong, but iirc, currently .NET generates a state machine for every await call.

I am not sure why the decision was made to develop a whole new approach, but was the option to replace the generated state machine with green threads even considered? This would not break existing APIs or language paradigms and would improve the existing applications' performance, as I see it.

@HaloFour
Copy link

@shybovycha

Correct me if I'm wrong, but iirc, currently .NET generates a state machine for every await call.

The C# compiler emits a state machine for every async method, but not one for every await call. Not all methods that return Task<T> or a task-like are an async method or have generated state machines, though.

I am not sure why the decision was made to develop a whole new approach, but was the option to replace the generated state machine with green threads even considered? This would not break existing APIs or language paradigms and would improve the existing applications' performance, as I see it.

From the technical details posted it sounds like the teams did explore this avenue, by having existing blocking I/O methods detect whether they were on a green thread and wiring up notification before yielding. They did rely on Task<T> and existing async methods in the BCL to a point but that was probably to having to refactor a ton of code. Green threads incur their own overhead, and also require some kind of state machine allocated on heap in order to respond to the I/O callback and wake the thread. I'd expect that even if the team didn't use Task<T> and some existing machinery that it would still have been less performant, or a wash at best. Only way I think the team could prove that would be something more synthetic and rewriting as low of an I/O call as they could using completely different machinery.

@qouteall
Copy link

@rogeralsing If I understand correctly, the main cost of increased foreign function call overhead comes from stack switching. Green thread is initialized with a small stack and grow by-demand, to reduce memory overhead of having many green threads. The called native code does not have stack growing functionality, so not switching stack could cause stack overflow.

Golang does stack switching when calling FFI which is also slow.

@obratim
Copy link

obratim commented Oct 16, 2023

I would like to remind that async/awit serve not just for performance, but more importantly to describe the behaviour of the program, so that programmer himself could later understand what program is doing

I, personally, dont want the runtime to implicitly create and run some "green threads"

I am afraid that the productivity of a develper would only decline with sutch feature because programs may become more bug prone

@Jeevananthan-23
Copy link

Hi, I'm junior system dev who want C# be a better System Programming Language. C# current Async state machine model is really good to handle I/O bound which running in user space(not kernel). Where the recent evolation of system langs like Rust/Zig much care about high performance, control and most imp Memery safety. More over io_uring in Linux space and IOCP in Wids space way to go for thus langs to async work more fater. And I'm hearing some news about Microsoft integrating and investing more in Rustlang which is good alternative for C++ where the CORECLR is writen in am I wrong here ?

After the Green Thread result it's clear that async programming is more faster better to work on async model to beat Golang.

Pointing out zig issue: ziglang/zig#8224

@sgf
Copy link

sgf commented Jan 8, 2024

I think the caller should decide whether to be asynchronous or not, which means that the API should all be synchronous. When the caller wishes to make asynchronous calls, the compiler wraps them as asynchronous.
The current asyn/await+Task combination is obviously a very intrusive design. This is very unfriendly and even breaks up the ecology.

So as far as I can see either .net style :
response.Body.WriteAsync(payload).AsTask();
Or the style of green thread:
response.Body.Write(payload);
Neither is good enough.
.net styles require the coordination of underlying libraries.
And the same goes for Green Thread. From a grammatical level only: The advantage of green threads is that the underlying asynchronous logic can be called externally in the same way. But the disadvantage is: the outside cannot see whether it is an asynchronous call.
The advantage of .net is that it can be seen from the outside that it is an asynchronous call, but the underlying library needs to be modified on a large scale.

@HaloFour
Copy link

HaloFour commented Jan 8, 2024

@sgf

I'm not sure what you're suggesting there. Without callbacks (facilitated by async/await coroutines) or green threads, you're back to blocking kernel threads. There's nothing that the caller can do there, short of spinning up a separate thread to run that code, which is exactly what we're trying to avoid given it's expensive and wasteful.

The closest you get to allowing the caller to determine whether it's async or not is via green threads, by virtue of the caller having to run within a green thread in order for the asynchronous method to be able to park the thread at all. But you still run into the problem that you need the entire ecosystem under that method to be written in a manner that supports notifications and unparking the green threads. That requires splitting the ecosystem, whether that be through relying on existing asynchronous APIs, or having the methods manage in internally. Otherwise, you're back to blocking kernel threads, even if they happen to be executing a green thread.

Either way, everything under the hood has to be written to be async-friendly. It has to call specific APIs that support notifications and handle all of the plumbing to resume operation. Both approaches are necessarily viral (tasks or green threads all the way down) otherwise you still block kernel threads.

@NCLnclNCL
Copy link

NCLnclNCL commented Jan 10, 2024

When will have it, i dont want use async, await method and normal method, which need 2 method

@sgf
Copy link

sgf commented Jan 13, 2024

@sgf

I'm not sure what you're suggesting there. Without callbacks (facilitated by async/await coroutines) or green threads, you're back to blocking kernel threads. There's nothing that the caller can do there, short of spinning up a separate thread to run that code, which is exactly what we're trying to avoid given it's expensive and wasteful.

The closest you get to allowing the caller to determine whether it's async or not is via green threads, by virtue of the caller having to run within a green thread in order for the asynchronous method to be able to park the thread at all. But you still run into the problem that you need the entire ecosystem under that method to be written in a manner that supports notifications and unparking the green threads. That requires splitting the ecosystem, whether that be through relying on existing asynchronous APIs, or having the methods manage in internally. Otherwise, you're back to blocking kernel threads, even if they happen to be executing a green thread.

Either way, everything under the hood has to be written to be async-friendly. It has to call specific APIs that support notifications and handle all of the plumbing to resume operation. Both approaches are necessarily viral (tasks or green threads all the way down) otherwise you still block kernel threads.

I think the go language handles this aspect very well. It has no magic at the usage level and looks simple. The difference between synchronization and asynchronous is just a go keyword.
The design of the Go language embodies the design of dependency inversion in programming languages. Whether asynchronous is supported depends on whether the caller needs it, not whether the callee provides it.

And the goroutine+chan+select method can be applied to most situations.

If possible, may be able to use the @ symbol to represent asynchronous calls (currently the @ symbol should only have a keyword escaping function when calling a function prefix? But we might as well add this function), or maybe can directly implement the go keyword of the go language.

@CallMathod();//Use the @ symbol to tell the compiler that a synchronous method needs to be called asynchronously
go CallMathod();//Use the go keyword to tell the compiler that the synchronous method needs to be called asynchronously.

We already have BlockingCollection and Channel
Then we will implement a tool related to the Select mode, and it seems that we can perform programming similar to Go.

Of course, my understanding of the Go language is still a few years ago.
But one thing I know is that the Go language is currently being widely used in IO-intensive demand areas such as databases, docker, and network applications.C# in these areas,Although it cannot be said that it has no achievements, it is still very rare.

@sgf
Copy link

sgf commented Jan 13, 2024

When we do something asynchronous, we don't necessarily need a callback. Often we just need a result, which is a concurrent structure.
For example, BlockingCollection is used to receive asynchronously returned results.

rlt is an AsyncResult<BlockingCollection<T>> or AsyncResult<Channel>

var rlt= go CallMethod;//Asynchronous execution
var rlt=@CallMethod;//Asynchronous execution

CallOtherMethod();  //executes other synchronously

if(rlt.OK/Error/Other) is similar to await, here you can wait for the result synchronously.

@HaloFour
Copy link

@sgf

I think the go language handles this aspect very well. It has no magic at the usage level and looks simple. The difference between synchronization and asynchronous is just a go keyword.

The difference with Go is that everything is a green thread. The process starts on a green thread, and go launches more of them. It's always non-blocking because you're not given any choice in the matter. At best you're given the illusion, but given results would be propagated back via channels or callbacks that distinction doesn't even really matter.

But one thing I know is that the Go language is currently being widely used in IO-intensive demand areas such as databases, docker, and network applications.C# in these areas,Although it cannot be said that it has no achievements, it is still very rare.

I suspect that ASP.NET is used much more widely than Go for server-side applications.

@vukovinski
Copy link

vukovinski commented Jan 13, 2024

My 2 Euros:

I think we should start thinking about CPU compute threads and PCI signals as the un-parking mechanism (flags).

Especially curious to see how one motherboard would differ to another one performance-wise.

Edit: then we could use the GPU as a memory bank when not handling heavy graphics.

Proof: Ideally cubical computer. Asymptotic n-linear space. Threads are being spawned out through (managed?) IRQs. Regular logic. QED.

Controversy: It's not a geometric logic, yet.

Codename: #KernelNT2

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
area-green-threads Green threads
Projects
None yet
Development

No branches or pull requests