Asynchronous Programming in C#
github.com
github.com
There are a ton of rules to memorize, compared with using a language such as Go that hides the complexity of async. Linters can help check and correct code, but they can be cumbersome to setup.
I wonder if there will eventually be a new mode for C# or even a new language for the CLR that does away with sync I/O completely, and makes async easy to do correctly.
Not an argument for one or the other. Go's restrictiveness can be seen as a benefit, C#'s flexibility can be seen as a time waster.
> ...or even a new language for the CLR that does away with sync I/O completely, and makes async easy to do correctly.
Well, your caveat "for the CLR" means I have nothing to suggest. If it weren't for that, there are tons of languages (new and old) that approach concurrency differently.
async/await in C# is easy. Trying to be clever and reinventing the wheel, arguing with a compiler or not going for the simplest and most straightforward option even when you are presented with one makes it not so.
Have a "main thread"-style flow of execution that doesn't want to be async nor is a UI thread (which has special handling)? Just .GetAwaiter().GetResult() it. Or vice versa - block the worker thread if you really want to.
The reason the advice exists like the one being linked is developers abusing certain areas has historically been leading to proliferation of very sloppy code. Threadpool is perfectly happy if you have a particular heavy blocking syscall, like DNS used to be. It will work just fine, auto-scale worker threads to reduce work item pressure in queues and otherwise proactively mitigate this.
You don't have to worry, even if people ask you to, often out of puritanical considerations (like not mixing async and sync, even when it's the easiest and not an issue).
That won’t work with various synchronization contexts, where doing this would cause a deadlock. There’s not much fun in trying to debug such issues.
And now that various libraries only provide async api, or worse an non-async version wrapping the async one with . GetAwaiter().GetResult(), you’ll be in for a treat updating your dependencies.
Async all the way is the answer, although various frameworks still don’t offer async hooks. Recently I ran into this for example trying to write an async validator in blazor, but that’s not possible and you have to work around it [1].
C# 5 introduced async/await almost 12 years ago. And we’re still not “async all the way”.
Or developers that used to work with something long time ago and never revisited their knowledge.
Or participants in the conversation which have strange, inappropriate and unrealistic expectations pitted against .NET but not any other technology, which would go out of their way to post something that is factually incorrect, simply out of desire to be controversial (I can shit on Go all day, but I'm not going to lie about its pros and cons).
In my 10 years of experience writing in a language that I like and I think I know rather well, I cannot share your view that “async/await is easy”. The happy path is easy, but you’ll hit show-stopping issues when you’re building big complex systems.
For that I find the explicit async/await quite valuable. Could non textual code highlighting in modern editors do that as well: Sure, but that is not the tech of 10 years ago when C# team came up with that.
I don't thing async helps with this. People will always write code like everything is in memory. My feeling is the best antidote is to artificially introduce worst case latencies even when developing locally so the pain will be felt during development instead of production. Perhaps static analysis could help as well.
> the complexity of getting async “right”
For most single-context use cases, it's no harder or difficult than async/await in JavaScript and hardly anyone would call JavaScript "complex". Promise (JS) = Task (C#)
Promise<T> (TS) = Task<T> (C#)
Promise.all() = Task.WhenAll()
For multi-context use cases, there's just a bit of awareness required to understand that you may need continuation on the original thread and awareness of context-sensitive resources. But if you're building web API backends, most of the time, it's not a concern and async/await works (mostly) like JS because the dependency injection container is playing a big role in handling object scopes.C# has to be one of the more approachable languages for most teams that are already versed in JS/TS because of the similarity[0] and influence of C# on JS and TS (JS async/await came after C#; same for JS lambdas)
This is not complex at all once you grasp the basics. It all trickles down from there.
var a = await AlphaAsync();
var b = await BetaAsync();
var g = await GammaAsync();
will be slower than the equivalent synchronous code. It does the same work, in the same order, and will not run any part in parallel. It can theoretically scale to more concurrent connections and in-flight requests, but I had a tough time achieving a measurable benefit even in benchmark tests.To get things running in parallel, the tasks have to be started but not immediately awaited:
var ta = AlphaAsync();
var tb = BetaAsync();
var tg = GammaAsync();
...
var a = await ta; var b = await tb; var g = await tg;
I've only ever seen this turn up in real code maybe once or twice. People just don't do it, and the samples and documentation doesn't show this pattern either.In fairly extensive tests I found that it is actually pretty rare for threads to be the limiting resource, so async provided no benefit at all.
You pretty much need this specific scenario:
- A large auto-scale pool
- Tuned well to keep load at 80-90%.
- High concurrent connections per instance.
- Slow dependencies that return small volumes of data.
- A much larger back end than the front end. Think 100 VMs at the front and 10K at the back.
Violate any of the above and the benefits seem to evaporate. I’m sure that this scenario is common at FAANG sized orgs, but is extremely rare elsewhere.
The other one is that it is not always clear the context on which you can use certain APIs.
The language is truly lovely to work in, moreso than I think it gets credit for unless you've a lot of experience with it. It's just very thoughtfully designed, and MS support their own components really well and for a very long time.
The problem is that NuGet never became npm, gem or pip. The open-source community never really embraced C# (for understandable reasons) and it meant that the void there was filled largely by projects that couldn't get much traction and enterprise component vendors like Telerik.
I'm not in any way knocking companies that make their money selling these components by the way, I just think the message that people looking from the outside are seeing (rightly or wrongly) is that there's a tax payable to third parties on absolutely every level of this stack so why build with it? What's my incentive to start learning it when I can pick up node / React / Next / Python / etc. for free and start working right away with free tools? And that all shows in the ecosystem.
OSS adoption is an ongoing process, that doesn't happen overnight. The attitude in this and adjacent comments is a good showcase that even after 8 years, once your average developer has settled in a particular mindset, it becomes a matter of enforcing tribal consensus even when the facts change.
If you also ever actually used .NET and nuget packages (in a post-netframework period), it would be apparent that the statement is simply not true.
If I need a package to perform a task that I think other people surely must have brushed up against in the past then I know it almost certainly exists in npm. That’s just not true for NuGet, as much as it would be better for the ecosystem if it was. This shows in the contributions to each, and the contributions (in terms of volume) are much higher for npm.
I specifically noted the post-.NET Framework experience. It is a continuous problem that community demands from the standard library and/or sdk to ship something it has no business shipping (like the sibling comment complaining about System.Drawing, it is truly an 8th OSI layer issue). And when there's an OSS solution, there is a stark difference in comments between developers who understand that OSS projects are projects of collaborative development (meaning that submitting and issue you care about may involve a degree of contribution) and the ones which demand paid level of support for free.
JavaScript is also just generally massive. Surely you're not going to hold it against Rust that Rust projects are less frequently contributed to? C# and .NET are unique language and platform which do overlap with Java and JVM, and other similar class of tools but do solve problems the latter cannot, or solve the ones that they can but in a frequently better way (for example: trying to reach performance ceiling in Java requires way uglier tricks with worse results rather than writing clean byref/pointer based C# code with structs and struct generics).
Here’s 8 third-party libraries I have published on nuget in the last ~7 years: https://www.nuget.org/profiles/ConstMe
Only one of them is an SDK for a cloud service, but it’s unofficial SDK. I have no affiliation to pcloud, I only use the free version, I just carefully implemented the documented TLS/TCP binary protocol to access their cloud.
None taken.
> computervison project in c#
Yeah, for CV applications nuget.org is indeed not particularly great. Very few people are using C# for these things, people typically choose something else like Python and OpenCV.
BTW, same applies to ML libraries, most folks are using Python/Torch/CUDA stack. For that hobby project https://github.com/Const-me/Cgml/ I had to re-implement the entire tech stack in C#/C++/HLSL.
The biggest example are PDF producing/converting libraries, which I have no doubt make millions in licensing.
It's a very pleasant language to work in, especially compared to Java, but there are so many other really nice C adjacent languages in the field that it's hard to argue for if you're not in that ecosystem.
It appears to have been the case since .net 5 (2020) (I've also stopped paying attention to MS a bit before then).
"Yes, .NET 5 is cross-platform and can be installed on Linux, Mac, and Windows. It aims to provide a consistent development experience across platforms by unifying different .NET flavors, including .NET Core, .NET Framework, .NET Standard, and Mono. .NET 5 also aligns all frameworks to support a common set of APIs, making it easier to build cross-platform libraries."
So in theory it's all independent but in practice the best bits are still on Windows.
There are decent third party open-source libaries like SixLabors/ImageSharp that have stared to fill the gap.
Missing a Graphics Library in .NET (5-8) is just half the story. It's of course missing an entire UI library too. But Microsoft, perhaps wisely, chose not to also implement a whole xplat UI library. Avalonia/Uno/etc are already there.
This depends a lot on what you want to do in your program. Of course, for example, for interacting with other Microsoft technologies, .net has a better ecosystem.
About as easy as Python, at least.
Which, for MS, made a lot of sense, they wanted to hook win32 apis. Java doesnt care, as much...
unsafe
{
[DllImport("libc", EntryPoint = "write")]
static extern nint Write(nint fd, byte* buf, nint length);
var text = "Hello, World!"u8;
fixed (byte* ptr = text)
{
Write(1, ptr, text.Length);
}
}For example, even probably their best product VS Code only got reasonable multiple screens support last year: https://github.com/microsoft/vscode/issues/10121#issuecommen...
And then, on the other end of the spectrum, you have Teams and ecosystem surrounding it. The "fun" of Teams bot integrations and the mess with SDKs and SDK documentation for it — never again.
It took several versions of the language to have readonly and records. Non-null references are just a bunch of compiler warnings rather than a first-class type.
Generics work, but type constraints are a joke. In particular, the ~new()~ constraint shows the disregard MS has always had for object-oriented programming: objects are supposed to be fully constructed; parameterless constructors are to be the exception, not the rule. On a similar note, record struct insist on having a parameterless constructor that cannot be disabled.
Furthermore, the language is mildly-typed (I'd say strongly if it wasn't for non-null references), but the framework is not typed at all! How people could write "public virtual bool Equals(object other)" with a straight face is beyond me!
Well it was created before generics in .NET one and can't be removed, that's the simple answer. But yeah, making object equality customizable virtually was probably the worst design mistake in C# and .NET. Having to specify the comparer explicitly when creating e.g. a Set collection would have been so much better for quality.
The second worst mistake was defaulting CultureInfo to the system culture (i.e. making methods like float.Parse()/ToString() take the system culture into consideration instead of requiring everyone to pass in the required behavior)
Then I found out about the ability to introspect and modify lambda expressions and generate code at startup time, which obviated the need for a template with constrained new, and I was blown away by that power.
I was also very pleasantly surprised that C# had very easy to use multiple dispatch.
So, C# is an extremely powerful, but you can't always port C++ idioms 1:1.
edit: and before you ask, yes, I'm a recovering complexity addict.
.NET 1.0 was a direct copy of Java, so these Object methods are inherited from there.
It's compatible enough that it's possible to transpile Java Bytecode to run on the .NET runtime and vice-versa.
If M$ is bad, Oracle is Beelzebub.
But, I'm still not entirely convinced Microsoft has changed their ways (not that Oracle is any better...), particularly because of the things mentioned here: https://isdotnetopen.com/
Last time I checked, they reversed their decision regarding the proprietary vscode extension, which is great, but the debugger situation hasn't changed & that makes me question whether it's worth switching.
The article would have been interesting if it had proposed a solution to this problem, but instead they just brushed it away as if it was pointless.
> There are very few ways to use Task.Result and Task.Wait correctly
I don't even speak C# and I can read between the lines enough to see that they're saying 'waiting for a non-blocking method turns it into a blocking method, undoing the work of the library implementers who worked hard to provide you with non-blocking code'.
Looking at the article, I think the solution was covered in the first sentence of the first list item:
> Asynchrony is viral
> Once you go async, all of your callers SHOULD be async,
If you can't fathom that calling code could be written before, or independently from, or in a different domain than. called code, than you've never developed complex code.
So basically: you have to call async from a synchronous context. But I think perhaps the article correctly avoids the topic, because in 99% of cases when you think you need to do this, you don't. And having an easy googleable page with an easily copied recipe for doing so could do more harm than good.
In the end it's going to be extremely rare that you can't bend over backwards to refactor your callsite to be async. E.g. you are in a constructor (which is sync) and want to call an async method. The solution isn't to wait on a result, the solution is obviously to not call it from the constructor. And that's the solution even if you have to refactor for a week instead of a blocking wait.
But yeah I agree he could have included the answer, surrounded by big red warning sigs that you probably don't need to, or want to copy it.
Perhaps the key clue is to give the good pointers to solving it properly. Just saying "you have to be async all the way" isn't really helpful. One key insight I got from somewhere was that identifying the async starting points was the key. E.g. for a desktop app, the realization that in 90% of cases your async "root" will be a GUI event, and that these event handlers in the GUI frameworks like WinForms or WPF can be async whenever you want. As a last resort, your main() method can be async.
The community consensus is to do `GetAwaiter().GetResult()` and simply accept it's not an ideal usage.
The ideal solution is to split the code and continue the execution through an event/continuation/queue, etc...
I'm sorry, it looks like you did not get the problem: you receive a Task, and you are in a sync environment, and you don't have control over these two constraints. How do you safely wait for the Task to complete?
A solution is obvious – use await instead. Modern versions of the language even support async main function.
Modern asp.net is designed in async-only way, no impedance mismatch there.
async void functions are compatible with non-async delegates. This solves impedance mismatch for GUI frameworks or other event-driven apps. Just don’t forget to catch and handle exceptions from the async code inside, also for GUI apps show users that background operation is ongoing.
The standard library exposes both flavours of APIs for files, sockets and many other built-in classes. You don’t have to use async APIs unless you want to.
I just happen to be sitting here reading HN instead of fixing a deadlock I've run into.
I have a ton of async code and I just now noticed that my projects don't use this analyser.
I dont know why it's not included by default but I'm gonna add it for sure.
FYI this is written by David Fowler, who is personally responsible for the creation of ASP.NET Core, SignalR & Nuget (maybe?).
> SDE II 2010 - 2011 1 yr Co-creator and founding developer for NuGet package manager. Designed and implemented the dependency management engine for the core NuGet as well as several other features such as the initial NuGet server implementation.
I should start listing projects I worked on at early stage as creator.
Claiming to be “creator” is disingenuous. Worked on it. Yes. Big contribution over a year before moving onto other things. Yes. Creator? No.
He wrote the core dependency management engine among other things. His contribution only seems small now only because the project has been around a long time with many new contributors since he’s moved on.
But you can rewind the git history to see he had a huge impact as a creator. I say this as the PM of team that built NuGet.
Oisin
On the other hand maybe the success Microsoft has had lately is expressly BECAUSE there are not too many cooks in the kitchen?
This is news to me. AFAIK one of the problems is that exceptions in that method will go unnoticed because they never reach the root thread when they occur. They might only become visible, wrapped in an UnobservedTaskException, when the process shuts down.
Edit: Maybe the author meant that the unhandled exception will cause an invalid system state and thus cause other exceptions? Or maybe I'm just completely wrong here.
I usually add an event handler to TaskScheduler.UnobservedTaskException that logs any exception like that so at least there's some trace of it. You could also use it to end the process with an error this way if you prefer to treat uncaught exceptions as fatal errors.
(I personally can't remember using "async void;" seems like something like a goto that technically has a valid use case, but it's simpler to avoid.)
This is my biggest misgiving with C# async. What fraction of projects can actually "just" refactor everything, all at once? Then first class constructs like HttpClient are async, so devs are forced to either do the wrong thing or use an older, far less commonly used class to achieve the same thing.
"Viral" features than can't be introduced incrementally in a code base should be a non-starter.
IMO it is a great alternative to simply deprecating/breaking older things and will slowly push the ecosystem towards .NET's recent language features, which will improve readability. If a company is not putting a .NET Framework -> .NET upgrade in their near future (which is a great time to go full async), you probably don't want to be in that job for too long.
I was responsible for one of the largest .NET to .NET core migrations in history (3M LOC). Not a good time to also make everything async "while you are at it".
Yes, because that's the majority.
You will probably not see another major upgrade from .NET to new-shiny-thing in a decade or more, so you may as well take the extra time to do things right for .NET now, even for the largest projects.
There have been multiple LTS's to target, and once you are over the initial Framework -> .NET "hurdle", upgrading from there should be much easier.
>Larger projects can't survive a "stop the world" / "go async" project.
I don't know how it would be fatal unless the company doesn't want to give you the time for it - either because it doesn't make financial sense or they haven't been convinced. .NET is certainly one of the easier languages to refactor with confidence.
If it will never make financial sense, then I guess it'll go on to join the still-running COBOL projects, but that's an outlier.
Even Microsoft can't manage to migrate it to modern .NET.
For most cases, upgrading to Core is long overdue, but also that's a very good time to change as little as possible to get it to compile and work. Only then can you think about the massive refactoring of async.
> This is my biggest misgiving with C# async. What fraction of projects can actually "just" refactor everything, all at once?
You don't have to.Simple heuristic: greenfield? async/await all the ways down.
Brownfield? Block.
Easy.
That's not what "forced" means. That's a choice.
> "Viral" features than can't be introduced incrementally in a code base should be a non-starter.
This is the "forced" case. No choice.
The concerns raised are niche and edge case and task must always be forwarded as is provided there is no post-processing or resource cleanup with idisposable.
I wish Mr. Fowl had more time to modernize the guide and remove other snippets that are no longer recommended like creating long-running tasks.
(it is more efficient, and threadpool can perfectly cope with it, to just moderately block the worker threads when no other option is available, and nowadays there are better tools like Channel<T> over BlockingCollection<T>)
Can you justify further?
The correct approach to this is await return has to be justified. Doing more work has to justify why this work is being done - because return await produces a whole another state machine box and async call overhead. Something that future async2 project will solve without any code changes, but hasn't become the reality just yet. The exact example in the advice is an anti-pattern (single-statement method call) that some analyzers will luckily complain about.
In order for the advice to be correct the method body should actually have either AsyncLocal declaration (exceedingly rare), or care about exact stack trace (also rare, do not rely on this as it's not guaranteed either way). If the code has a disposable, there will be a warning that returning a task of out using may be undesirable (it is common knowledge at this point).
Hence the example is as much of a mistake as doing `return await Task.FromResult(value)` over just `return value`.
We have some of these as well. Thanks, seems like it's time for some introspection.
(Now, you might be thinking, "no, that won't work because of XYZ corner case," and I would say that: 1: If those corner cases apply, then you should be writing both versions of a method; and 2: For the vast majority of APIs that need to create both sync and async methods, the only difference between the methods is dropping "async ... Async" from within the implementation.)
Or, alternatively, one thing that surprises me about C# is that the JIT can't support calling "async from sync:" IE, it can't, at the lowest levels, swap in a blocking call. (So you don't have to write separate sync and async APIs.)
> 1) Once you go async, all of your callers SHOULD be async, since efforts to be async amount to nothing unless the entire call stack is async.
> 2) In many cases, being partially asynchronous can be worse than being entirely synchronous.
> 3) Therefore it is best to go all in, and make everything async at once.
I agree that all callers should become async. But I disagree that all callees should also be async, which is what 2 and 3 seem to imply.
You don't async for async's sake; you async to route around delays caused by blocking calls.
In their good example,
public async Task<int> DoSomethingAsync()
{
var result = await CallDependencyAsync();
return result + 1;
}
they've included a line of business logic after the async machinery. If this logic grew to more than just (result + 1), I'd expect that code to be in a separate synchronous function.I probably also wouldn't write out DoSomethingAsync() in full, either in the simple example above, or in a version with more complicated result-handling. Most of it is boilerplate: start a function call one function un-async the result into one variable do one thing to that variable repack it as async return it
If you want 'one bigger than CallDependencyAsync()', why not:
CallDependencyAsync().ContinueWith(r => r + 1)
and if the logic grows, then: CallDependencyAsync().ContinueWith(r => handle(r))
private int handle(int result) {..}That's what you and I wrote. That's not what the article wrote.
"efforts to be async amount to nothing unless the entire call stack is async"
The entire call stack?
"it is best to go all in, and make everything async at once."
Go all in?
This beyond 'a little bit unclear' into 'incorrect' territory, especially since the preceding paragraph said "there's been lots of confusion on the best practices for async and how to use it properly."
Is there a single NB or asterisk-like comment on the whole page that walks back the "all in" comment to something like "calling sync from async is fine." ?
He makes no direct comment, afaik, on calling sync methods from async methods. The reader is left only with
efforts to be async amount to nothing unless the entire call stack is async
and it is best to go all in, and make everything async at once.
and must somehow parse this as some of the call stack, and make some things async.Because ContinueWith is considered antiquated in modern C# and shouldn't be used for a multitude of reasons? https://medium.com/@joni2nja/why-you-should-not-use-continue...
(I figured `ContinueWith` was just dumb naming to avoid 'map' at all costs, just like how Java went with `thenApply`.)
Have you heard the word of our lord and savior, LINQ?
In the article linked above: > ContinueWith allocates more than using async await
Does LINQ not carry a similar overhead?
Does LINQ's map come with a plethora of problems like I'm told Task's map does above?
Should you avoid mapping because 'it's the old callback model' which doesn't belong in modern code?
Is map fit for purpose, or has it been disavowed?
Or you can use it with Tasks. https://itnext.io/fun-csharp-doing-linq-to-tasks-because-tas...
(caution: contains the M word)
More from Microsoft: https://devblogs.microsoft.com/pfxteam/tasks-monads-and-linq... ; referencing Lippert https://ericlippert.com/category/monads/
ContinueWith is not a broken "map". What people usually want is Select. https://learn.microsoft.com/en-us/dotnet/csharp/linq/standar...
All good and informative responses that provide actionable feedback to raised concerns are downvoted, and simply incorrect or outdated statements are upvoted.
Please try the language and the features it offers with modern templates first before leaving feedback that is based on assumptions and impressions about things completely unrelated to C#.
People are requesting that the author write 'the right way' to synchronously return from an async method, since he recommended against blocking... am I having a stroke?
That said,
> Please try the language and the features it offers
I did walk away with a much worse impression of C# after reading this article. I knew not to wait on async (because then it's sync) before this (By definition! in all languages!) But now I know that it might also deadlock.
The other thing I got from the article (not the comments) is just how much machinery and wiring is left lying there for the app developer:
AsyncLocals?
CancellationTokens?
Implicit async void delegates?
FlushAsync on StreamWriters before Dispose?CancellationToken is a relatively idiomatic way to propagate the cancellation of operation (there are better ways! other popular languages do it even worse however).
Implicit async void delegates is also an exotic thing, you deal with it even on a more rare occasion than asynclocal (you can certainly introduce such a gotcha, but I've never seen that in practice).
Flushing the the stream or a writer is common (does not relate to async per se) because whatever you are flushing might be otherwise buffered and may not issue writes on IO otherwise when you are done with it (well-behaved implementations might safeguard you with a double-check in .Dispose(), not-so-well-behaved ones might not).
Deadlocks are specific to GUI applications which have special synchronization context, it is also only relevant when you do so from a render thread. It's something all GUI applications have to contend with in one way or another, even if the abstraction and language are different.
This article just lists known quirks that exist which always accompany the inherent complexity of writing applications that do multi-threading, possibly GUI and that may want to do "ambient state". It exists for you to simply note "aha, asynclocal, check docs when working with it" and move on. Certain points there are certainly up to debate. Which is why I noted in another comment to just not worry about it and try to write simplest and most straightforward code first and then try to fix it if you have to (which is unlikely). As long as you don't fight the language and don't ignore the compiler warnings, it'll be alright.
p.s.: if you prefer Haskell, then F# is likely to be more up your alley