Description
Update
For the runtime-async feature currently in development, see dotnet/runtime#109632
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.