And of course C#/.NET is a bit older and also very large, it has more surface area for weird behaviour that might not be easily fixable due to backwards compatibility.
Things like using void as a return type for an async function can be a nasty surprise if you're new, but this is not an issue at all once you know this (or if you simply used the right examples and used Task from the start). It's not a subtle gotcha.
Sync over async is much sneakier and can be very nasty. But I'm not sure you can avoid this when interacting with a language/environment that used to be mostly sync and switched to async. If everything is async you don't have this issue, so this is better for newer codebases.
"Hey, this is an HTTPClient call? Put an await in front of it?" "Oh, is the IDE showing an error? Try .ConfigureAwait(false)?" "Oh, still some issue? Try putting async in the method declaration?" "Still showing an error? Remove the .ConfigureAwait() and just try async?"
At some point, Visual Studio would stop showing warnings and errors, and then the code would pass review.
Go is better in the sense that, at least people understand what a goroutine is and how/when to use it correctly.
I hope the examples you listed are facetious or from the very early days of async/await in C#, otherwise I'd seriously question the skillset of the supposed .NET programmers.
Visual Studio is fairly good at handling incorrect use of async/await, and in all of the examples you listed, the actual solution should've been "read the IDE error, hit the bulb and apply the automatically suggested fix", not "ignore the IDE error and smash keyboard until it works".
ConfigureAwait usage is also not something you'll usually see outside of library code in modern C#.
If you've ever had to deal with the IAsyncResult pattern in older .NET code, you'll never complain about await.
https://docs.microsoft.com/en-us/dotnet/standard/asynchronou...
The problem is that Task/Task<T> was the foundation for async, and it's a bad foundation. Even with the ability to write your own duck-typed awaiters (and the advent of ValueTask), the widespread use of Task means if you're writing async code you're going to have a tough time getting away from it.
Certainly the async story is a lot more complicated in desktop but it is very simple for most server scenarios, simply put "use this async call so that the thread can do other things while you wait for the db to respond" and the model in code is much preferable to callback hell.
Microsoft in general has a tendency to bolt on functionality in a kinda slapdash manner when another team wants it, so you get a lot of cruft that really doesn't belong in the Task class[1] but is there because someone wanted a way to handle their special case so it just got thrown into Task.
[1] See https://source.dot.net/#System.Private.CoreLib/Task.cs,045a7...
I haven’t used Go for a while, but you can’t await a goroutine, you have to use a channel, which is more complicated than just using ‘await’. C# has channels, so you can replicate Go’s model.
Multithreading is one of the hardest problems in software, and Microsoft decided that the best way to solve it is to get smart and experienced people to stop forget everything they know and instead learn a bunch of opaque apis that interact with an incredibly complex internal state machine.
It hardly seems worthwhile to me.
The most illuminating moment for me was when I realized that there is no multi-threading involved with pure async/await.
The canonical Microsoft tutorial on async spends about half its time talking about hiw to make your code concurrent to take advantage of async.
https://docs.microsoft.com/en-us/dotnet/csharp/programming-g...
Totally agree. At first look async/await is simple and straightforward but it's way to easy to mess up in subtle ways. Most people don't even notice that their code has problems until they get weird behavior in production.
In general I believe they made async way too pervasive in the framework and are also inconsistent.
With these newer programming models there are easier ways to distribute work but unless you really dig deep and understand the basic mechanisms you will be lulled into a false sense of security.
When async was first added to .Net I read through the details of how boldly the compiler re-writes my code and I was a bit shocked, like, can it really do that? Now I always keep that in mind as soon as I start typing a...
Honestly, just the first point "Asynchrony is viral" is a huge fucking flag that this implementation sucks.
It doesn't need to be viral, they just needed to make passing a continuation easy, and they failed miserably.
Overall - I really like most of C#, but that async/await implementation is poor at best.
Which means that .NET code tends to suffer from the same issue as Go code. The solution is the same as well: Separate out the code that does logic processing from the code that does I/O. This way only a few top level functions will become async. I find that this makes my code cleaner and more testable as well.
This is basically what some language communities are trying to capture with “monads” (like “the IO monad”)
There are some work yet on how to make such representations compose[1] (like how IEnumerable + Task = IAsyncEnumerable) but eventually we’ll probably see some form of effect systems for all such things reach mainstream languages.