Stephen Cleary writes about asynchronous programming and concurrency in C# and .NET, with some posts on Docker and GCC.
HN commenters call his async posts brilliant and cite them for explaining async void, deadlocks, and why there is no thread in async.
I’m not picking on you specifically, it sounds like you do a lot of desktop .NET development and there are legitimate complaints there.
However, “too invested to switch now” reminds me of .NET/C# developers that don’t embrace change and/or don’t swim in other waters.
Some folks with a traditional Microsoft/Windows/.NET Framework mindset have real trouble with these things:
>“Windows and Visual Studio”
Rider, Linux, Docker, bash terminals, and MacOS are all things that can improve your ability to solve problems with .NET. I’ve met dozens of people that refused to even try a different OS/IDE and fail to gain the benefits of those tools and the broadening effect they have on your perspective.
>”too many frameworks”
This complaint sometimes stems from an unwillingness to learn web/mobile frontend technologies. On the server-side, ASP.NET continues to simplify— you can write an API now in one file with a handful of lines of code. [1]
>“limited core libraries”
The first party support in .NET is already pretty comprehensive. Some folks have an aversion to using open source packages, but this is exactly how you get things like YAML parsing in most other languages. Microsoft’s move to open source has been awesome. You can literally interact & contribute with the team’s that make the framework.
>”command line first”
This sounds like a feature to me. This allows for automation and is easier to document without a series of screenshots. Having a common CLI allows a team…
I've been working in .NET/C# for the past few years, and while I'm happy with it, I still think the JVM/Java are the best ecosystem overall I've worked in. It's amazing how many things the Java ecosystem gets right that .NET gets wrong.
For instance, Java introduced the fork/join pool for work stealing and recommended it for short-lived tasks that decomposed into smaller tasks. .NET decided to simply add work-stealing to their global thread pool. The result: sync-over-async code, which is the only way to fold an asynchronous library into a synchronous codebase, frequently results in whole-application deadlocks on .NET, and this issue is well-documented: https://blog.stephencleary.com/2012/07/dont-block-on-async-code.html
Notice the solution in this blog is "convert all your sync code to async", which can be infeasible for a large existing codebase.
There are so many other cases like this that I run into. While there have been many mistakes in the Java ecosystem they've mostly been in the library/framework level so it's easier to move on when people finally realize the dead end. However, when you mess up in the standard library, the runtime, or language, it's very hard to fix, and Java seems to have gotten it more right here than anywhere else.
Yeah, I should clarify what I was saying.
I wasn't calling Task.Run(), but I was doing:
for (var i = 0; i < 1000000; i++) {
var unused_task_var = RunTaskAsync();
}
await RunMainMonitoringTaskAsync();
And I purposefully wasn't calling await on "unused_task_var". And this was doing what I wanted, running all those tasks on multiple background threads in the pool, as long as RunTaskAsync method itself yielded once early on in its function (to return control back to the for loop).
tl;dr If you call an async method and don't await on it, it will be parallelized - for a console app, at least.
Honestly, I don't find the MS docs on this to be that great. Not 100% sure I'm even doing it the right way here. Everybody says use Task.Run but that is for CPU bound tasks. I want to run a ton of IO waiting tasks.
edit: looks like I'm doing it right. See "Async Composition" @ https://blog.stephencleary.com/2012/02/async-and-await.html
This is great and despite what the article says, an actually useful technique. The reason this performs well is the .NET runtime does monomorphization when the generic type parameters are value types (primitive types like int or user-defined structs). In modern .NET you would use static abstract methods interface methods to implement this feature. But if you have to run on the old Windows-only .NET Framework, you need to use the approach used in BrainFlood. See this article [1] for more details. An example of an official .NET library that uses this technique on .NET Framework is TensorPrimitives. See this file [2], the structs are defined at the bottom.
[1]: https://blog.stephencleary.com/2022/10/modern-csharp-techniques-3-generic-code-generation.html
[2]: https://github.com/dotnet/runtime/blob/main/src/libraries/System.Numerics.Tensors/src/System/Numerics/Tensors/netstandard/TensorPrimitives.Single.netstandard.cs
> var myValue = DoSomethingAsync().ConfigureAwait(false).Result;
Came out a decade ago and we still don't know how to use it safely.
This example doesn't compile because there is no Result on ConfiguredTaskAwaitable. Regardless, ConfigureAwait(false) does absolutely nothing here because this Task is not being awaited.
If you're going to block this thread, you must push the work to another thread or it's going to deadlock when the implementation tries to resume a continuation (unless the implementation is 100% perfect and the SynchronizationContext smiles upon you).
var result = Task.Run(() => CalculateAsync()).GetAwaiter().GetResult();
This avoids the deadlock but can lead to other nasty things like thread pool starvation. The only true solution is to go async all the way - https://blog.stephencleary.com/2012/07/dont-block-on-async-code.html
Hey, so my my career path has been C# (many years) -> F# (couple years) -> Clojure (3 months). I understand multithreading primarily through the lens of async/await, and have been having trouble fully grokking the Clojure's multithreading. One of the commandments of async/await is don't block: https://blog.stephencleary.com/2012/07/dont-block-on-async-code.html
Which is why the async monad tends to infect everything. Clojure, as far as I can tell so far, doesn't support anything similar to computation expressions. So I'm guessing your "poked at it a couple times" was something like calling `pmap` and/or blocking a future? All my multithreaded Clojure code quickly blocks the thread... and I can't tell if this is idiomatic or if there's a better way.