> Also, the async/await approach, such as Kotlin coroutines, has its own problems. Even though it aims to model the one task per thread approach, it can’t rely on any native JVM construct. For example, Kotlin coroutines based the whole story on suspending functions, i.e., functions that can suspend a coroutine. However, the suspension is wholly based upon non-blocking IO, which we can achieve using libraries based on Netty, but not every task can be expressed in terms of non-blocking IO. Ultimately, we must divide our program into two parts: one based on non-blocking IO (suspending functions) and one that does not. This is a challenging task; it takes work to do it correctly. Moreover, we lose again the simplicity we want in our programs.
> The above are reasons why the JVM community is looking for a better way to write concurrent programs. Project Loom is one of the attempts to solve the problem. So, let’s introduce the first brick of the project: virtual threads.
> An alternative solution to that of fibers to concurrency's simplicity vs. performance issue is known as async/await, and has been adopted by C# and Node.js, and will likely be adopted by standard JavaScript. Continuations and fibers dominate async/await in the sense that async/await is easily implemented with continuations (in fact, it can be implemented with a weak form of delimited continuations known as stackless continuations, that don't capture an entire call-stack but only the local context of a single subroutine), but not vice-versa.
> While implementing async/await is easier than full-blown continuations and fibers, that solution falls far too short of addressing the problem. While async/await makes code simpler and gives it the appearance of normal, sequential code, like asynchronous code it still requires significant changes to existing code, explicit support in libraries, and does not interoperate well with synchronous code. In other words, it does not solve what's known as the "colored function" problem.
This is under the section called "Other Approaches" in https://cr.openjdk.org/~rpressler/loom/Loom-Proposal.html
That seems to indicate to me that async/await would be an alternative to virtual threads.
I think that both approaches solve a lot of the same problems, but there is some problems that fit one paradigm better than the other and I don't think they are entirely mutually exclusive either. I've heard it mentioned with Loom it solves the async "colour" probglem however I think people "overblow" the color problem of the Async API too much. If most things are async it isn't really that painful. I actually like when something is marked "async" and I have to be careful how to use it. How is it async? Can the code run in parallel and join later? Is there race condition potential? Deadlock? How is it actually async, what's it sync vs async behaviour? How do I propogate the need to halt the async operation across multiple layers? Who owns the whole workflow, can I make sure the async context is propagated to that layer so they can cancel it for example?
Probably an unpopular opinion but knowing a method has async behavior, at the type level, could be a feature not a bug. It forces you to handle it at the sync/async junction and think about these things.
There are two problems with async/await's cooperative multitasking:
1. The first is only relevant to languages that also have threads: it splits the APIs into two worlds with very similar semantics but disjoint syntactic "universes." You always need a separate API for either world and need to do everything twice.
2. Even when that is the only paradigm, it is less composable. When scheduling points are explicit, adding a scheduling point inside a subroutine requires changing all of its callers, transitively. In contrast, with non-cooperative multitasking, any subroutine in the hierarchy can individually choose to exclude interleaving (and in a finer-grained way) for atomic operations with various constructs (the simplest being locks). Since most operations need not be atomic and are independent, this is not only more composable and evolvable, but also a more reasonable default.
If you design a language form scratch, async could be useful, but even then there would no need to annotate functions as they could be inferred. Generally effect systems seem a superior solution anyway.
Once the early code Node became actually used the callback pyramids of doom started gaining notoriety, there was even a small period before async/await where people abused generators achieve a similar style of code(The name of the Koa framework was a play on generator co-routines) ,luckily everyone migrated over afterwards.
Hopefully I'm wrong and there's a new golden age of GUI programming in Java but I'm currently skeptical.
Of course some specific implementations can be single threaded only (js) or use shared nothing threads (rust), but that orthogonal with the model
If you call a plain function from an async function, you know it will not run on a different thread no matter the executor. In this Loom model, you don't know that.
You control the thread until you yield execution no matter the executor. Explicit async methods also means you get explicit sync methods. In that sense, you will not be pre-empted when code changes under you. You can write async tasks that you know will run atomically without locks because you never yielded execution.
In this implicit model, I don't believe that's the case. Because yield points can be added at any time and thus you can be pre-empted at any time, you need to use locks for atomicity (or give up and revert to using a normal Thread). I don't think its possible to interweave this implicit yielding with atomic execution in a way that's as simple as calling a sync function. Is there some other way?
If you do not want preemption disable it. For example in posix threads use sched_fifo and pin to one core.
Loom might provide similar features if needed.
Basically, with async/await you need to allocate on the heap frames for each level of the async/await call stack. You basically get a linked list of stack frames.
This is essentially isomorphic to segmented stacks that Go ditched in favor of resizable stacks, because segmented stacks kill performance if a tight loop just happens to cross a segment boundary.
Are there any benchmarks to back that affirmation?
There's a bunch of benchmarks that compare C# async/await and Goroutines (e.g. https://alexyakunin.medium.com/go-vs-c-part-1-goroutines-vs-... ). Go still wins there, but they all miss the very important case of nested async/awaits.
These microbenchmarks also focus on looking at Go channels versus other synchronization methods, which in my experience is not that relevant (Go channels just suck).
I guess I'll need to write a blog post.
Results: Go - 32ms, C# - 494ms. The difference also becomes bigger as you add more recursion levels.
Green threads are decoupled from synchronization.
What I do like to see in C# is something akin to goroutines from Go or actors in Elixir.
The mistake is that async/await is not equivalent to virtual threads: the former can do only a subset of what the latter can.
A task is equivalent to a goroutine.
Probably there are examples where Java introduced something first, but I don't know because I'm not so well versed in Java.
While similar and inspired by Java, I do prefer C# because is less verbose, requires less boiler plate, it generally has only one proper way to do things and is kind of jack of all trades, in the sense you can tackle any area of programming besides very low level systems programming - and It quite can reach that point, too if there will be a way to disable GC and allow manual memory management.
Web backend - check, web frontend - check, mobile apps - check, desktop - check, multi platform - check, embedded - check, games - check, VM - check, native AOT - check.
It also looks great in benchmarks: https://benchmarksgame-team.pages.debian.net/benchmarksgame/...
I am biased but since I recognize it, maybe you shouldn't downvote me just for that. :)
And what's even better than C# is F#, but that's too bad nobody likes functional programming or hire programmers to use functional languages.
Just a clarification regarding one statement which I know for sure is not true: Java team started working on records before C# started similar work.
I don't know about the rest.
That's how we've been able to avoid properties and just have algebraic data types, avoid async/await and do virtual threads, avoid string interpolation and just have safe string templates. This also allows us to keep the number of features in the language relatively low -- as we, and most of our users, like it.
>That's how we've been able to avoid properties and just have algebraic data types
By algebraic data types you mean product types or sum types?
I wouldn't call that a good example of sum types. You can use sealed classes or other mechanism in C# to emulate sum types. I was thinking about something built in the language.
Example:
type IntOrBool = | I of int | B of bool
In JVM land there is Scala and people do hire for it, more so than any other typed FP language, AFAIK.
JavaScript/TypeScript is probably the only ecosystem with comparable async support, unless you count golang which achieves a similar result with different ergonomics. I'm still on the fence on which I personally prefer.. I can see the appeal of not having the async logic pollute the callstack, but at the same time the magic[1] way golang handles i/o seems antithetical to its philosophy of being simple and explicit (for example, with respect to error handling).
[1]: https://www.reddit.com/r/golang/comments/xiu4zg/comment/ip77...
https://github.com/dotnet/runtimelab/tree/feature/green-thre...
java had ExecutorService and futures support for a while, it is just another synthetic sugar around the same solution, although Java's approach is much more powerful and flexible.