Async/await also splits the standard library and the ecosystem in 2 (blocking vs non-blocking, or blue vs red), and it can't automatically update the behavior of old code.
The introduction of virtual threads in Java also works well with "structured concurrency", as seen with Kotlin's Coroutines. Kotlin's approach to concurrency is also superior to that of C#, actually. But what's interesting about Java is that its evolution is often one that involves runtime changes, lifting all boats. Java engineers preferred pushing more changes in the runtime, and somewhat ironically, the JVM ended up being the true multi-language runtime.
Java is a good case study of how languages should evolve. It has extreme backwards compatibility, and features being pushed are assessed for how they impact the whole ecosystem, including libraries or languages not in Oracle's control. Project Loom was developed in the open, compared to what Microsoft usually does.
(if you want to take a look at good structured concurrency, you might be interested in Swift implementation)
The async/await syntax works with the language's other statements, but for a long time it had gotchas. It doesn't qualify as "structured concurrency", and it has the aforementioned issues — it's (accidentally) error-prone, it splits the ecosystem in two, and has no interruption model.
I am not familiar with Swift, but I think you can hardly beat Kotlin's implementation. This is a good introduction from Kotlin's lead:
Interruption is achieved through cancellation tokens and has to be handled by consuming methods. There is no way around it because interrupting execution at an arbitrary point would lead to all kinds of issues regardless of the language (unless it implements some form of transaction abstraction and rollbacks all uncommitted changes).
That’s a choice, but it is unfortunate for the programmer. The truly right way to do it all is like Erlang does - where all processes are cancellable and nothing bad happens.
In Java, you don't need to initialize those "tokens" manually, because the interruption signal is baked into Threads. Moreover, a lot of the standard library cooperates with Java's interruption, which is why you see plenty of methods throwing `InterruptedException`; and it's also reflected in types such as `CancellableFuture` or the `Flow.Subscription` (reactive streams). Of course, user-level code that isn't well-behaved, can end up catching InterruptedException, or resetting the interruption signal, without actually interrupting. This makes Java's interruption model somewhat error-prone, but it's workable, and at least it's baked in.
Note that interruptions could also be preemptive, as you don't necessarily need cooperation. If you think of the call-stack, or flatMap/SelectMany in reactive APIs, the compiled code could check the interruption flag automatically and interrupt the call chain.
And resource leaks aren't necessarily a problem, if the interruption protocol is well-thought-out. In Java, try/finally still works in the presence of interruption, since at worst you get an `InterruptedException`. It's not ideal because you can interrupt the interruption process, and in truth the ideal would be for interruption to be its own communication channel, complementing that of exceptions. But it's totally doable, and here I am familiar with several libraries from Scala's ecosystem, namely Cats-Effect, Monix, and ZIO that show it (with limitations imposed by the runtime).
Either way, what C# provides is basically next to nothing. In fairness, some C# libraries tried fixing it, such as Rx.NET, but it's not enough. And the aggregate result in the .NET ecosystem is that interruption is not something people design for. Like what to interrupt a network socket? This ends up being a setting, presented as a timeout in case of inactivity, as a configuration of the connection, instead of a higher-level generic function that can be applied on the consumer side. And the probability for resource leaks goes up actually, because in the presence of concurrent races, you really need interruption.
Set<File> s = new HashSet<File>();
s.add( new File( "c:/temp" ) );
s.add( new File( "c:/temp" ) );
System.out.println( s.size() );
C#, prints 2: ISet<FileInfo> s = new HashSet<FileInfo>();
s.Add( new FileInfo( "c:/temp" ) );
s.Add( new FileInfo( "c:/temp" ) );
Console.WriteLine( s.Count );
C#, test fails: string path = "/tmp/filename.txt";
FileInfo f = new FileInfo( path );
File.CreateText( path ).Dispose();
Assert.True( f.Exists );
f.Delete();
Assert.False( f.Exists );
https://en.wikipedia.org/wiki/Principle_of_least_astonishmen...Languages have their strengths and weaknesses.
C# isn't flawless. Java isn't flawless.
Also, async-await is not a good thing — virtual threads are superior in every way in case of a managed language.
async fn read_file(filename):
f = await os.open(filename)
let data = f.read()
await f.close()
return data
this is equivalent code with green threads: fn read_file(filename):
f = os.open(filename)
let data = f.read()
f.close()
return data
As you can see, async/await is pure syntactic noise, it doesn't convey any important meaning.Better implementations offer eager execution and allow to easily interleave multiple concurrent futures/tasks, like C#. Green threads on the other hand are a workaround to deal with blocking for the most trivial case of cooperative multi-tasking, offering little beyond that.
It's not really a type, otherwise it would be something like Future<T> in Java and plenty other languages. It is usually implemented as a transformation to a state machine.
Also, Loom is M:N and calling them green threads doesn't give you the whole picture at all. Not exactly sure what you mean by easily interleave -- functionality wise the two is more or less equivalent. You just get to keep your simpler mental model (and tooling) with virtual threads.
> eager execution, interleave multiple concurrent tasks
green threads give you that for free
> deal with blocking
blocking is an implementation detail; both async/await and green threads can be implemented on top of either traditional blocking IO, or callback/non-blocking IO
I'd be even happier to discuss concrete code blocks / examples, that's where the superiority of green threads truly shines
It appears every six months there must be new language features being added.
The last one, for declaring fixed size arrays in structs, with an annotation instead of proper grammar change is getting ridiculous.
There are a lot of languages you can use with the JVM, Closure is one of them, Groovy, Scala and Kotlin are some more. Kotlin is gaining a lot of traction, especially since it’s backed by JetBrains.
type erasure for generics is annoying
I too have spent significant time programming in both C# and Java. This complaint about type erasure in Java: When does it affect your daily life? There are so many craft workarounds available now that it hardly comes up anymore.Also, would your opinion of Java significantly change if type erasure was removed or never existed?
I think it is a worse choice, it's too ingrained in the language and ecosystem to change. That and a couple of other nits would likely make me pick Java over C# or Go if I was starting a new project going forward.
Adopting async isn't impossible at all, there is very little demand for it.
Many uncancellable threads + mutability + goroutine panic kill it all are serious issues for golang.