I like C# a lot, but the complexity of getting async “right” is a turn-off to new developers learning the language.
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.
It’s even a “turn off” for some of us more experienced devs. It’s annoying that it has to be “async all the way down”. It’s annoying that they make libraries that are async instead of leaving that decision to the developer, etc. Go’s concurrency solution is more enjoyable.
C# is definitely more flexible than Go, which is by design on both sides.
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.
While I agree for the early learning, it is also very valuable for the later learning ... since it puts focus on where you write crapy code. When you loop around a resource access code 10.000.000 times, then you go in/out of your process for that often.
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.
> When you loop around a resource access code 10.000.000 times
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.
Go simply cannot easily solve certain problems that async/await can. It is a weaker language in this regard that has to resort to channels and wait groups even if those are undesirable and unidiomatic. And when it faces a problem that requires async/await, the solutions are not pretty.
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”.
I'm so tired of developers that don't bother to read the text they are responding to and immediately assume that the other party in the conversation does not know what they are talking about.
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 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)
Something important to note is that code like this:
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.
If the async methods return quickly, then yes, there is some overhead. But the point of asynchonous methods is that they can perform slow operations while yielding the calling thread back to the thread pool. For example Alpha might be reading a gigabyte file, Beta is sending it over the network and Gamma waiting for a timer tomorrow. Async/await is a syntactic sugar to run this in a pleasant way.
Here’s the rub: that time will have to be spent no matter what. Async just “frees up the thread”, but the system as a whole will still have to do the same processing. Async doesn’t make a gigabyte sized I/O take less time or somehow “go away”.
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.
Offtopic but I really really like C#. Such a great and easy language to work with! It's a shame that people do not choose it over Java because of M$-bad rhetoric :(
Coming from C++, I hate C# for the nice things that they removed, like const, value-based equality operators, non-null references and powerful templates.
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!
> How people could write "public virtual bool Equals(object other)"
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)
As a C++ programmer, the first time I had to write some complex generic code in C# I was annoyed that a common C++ pattern of macros+templates for registration of factory functions wasn't directly expressible in C#. My recollection is fuzzy, but I hit exactly the inability to constraint new that you mention.
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.
Thanks, that explains my mixed feelings. C# is very good to me, I feel very productive, but it also feels like "stricter Python" or something, which is not exactly what I expected, after being exposed to C++ for a great while. C++ occupies such a weird mix of formalism and "here's a knife, do something with the knife", but I've found that since a few years back I almost never have a program crash on me in C++. Just logic bugs. I must have found treaded garden paths.
I also prefer C# to Java, but there are reasonable arguments for Java. While the language is inferior, the Java ecosystem is superior to .net, and this might be more important.
> the Java ecosystem is superior to .net, and this might be more important.
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.
As someone who's mostly worked with Java & the JVM, I've thought about switching to C# & .NET/CLR for a while.
To me C# & some of the tooling/frameworks seem really nice. What made me curious was the Avalonia framework for desktop GUI development which seems to be much better than JavaFX/Swing.
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.
It still has some platform dependency that Mono or .Net Core doesn't fully solve.
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.
I think that might have been the case a while ago but they have even dropped .net-core and it's now just .net and cross platform.
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."
Yeah but the way they've done it, iirc, is by cutting libraries out of Core that really should be in there. I remember looking recently at some pretty basic stuff that broke in 6, simply because for some reason the relevant APIs (which were pretty low-level and not really related to anything driver-specific) were marked as Windows-only and ripped out of Core and Host packages.
So in theory it's all independent but in practice the best bits are still on Windows.
It has been amusing watching them try to get cross platform going, I used to hear how they (c#/.net) did such a good job with backwards compatibility, however Java had been doing crazier platforms for longer and their cross platform/backwards compat is in a much better state... although they are much more difficult to use to integrate deeply with a system.
There are many metrics by which C# is the nicest language, but after 10+ years of doing (and loving) C# development, I find that I prefer Dart as a C#-like language these days.
The biggest one I've run into is GDI support (Graphics Device Interface). This covers things like two-dimensional vector graphics, imaging, and typography. So simple things like transforming or drawing images are no longer supported.
There are decent third party open-source libaries like SixLabors/ImageSharp that have stared to fill the gap.
System.Drawing was always just a wrapper for GDI(+), and you CAN use it on Linux via using System.Drawing.Common which is a wrapper for GDI+ (Windows) or something else like libgdi+ on non-windows. It works - but it feels more like a compat hack than anything else. System.Drawing wouldn't look the way it does if it wasn't designed to be a Windows only API around GDI+.
ImageSharp was always a good lib but now the license was murdered. Hopefully a true OSS fork will come along. SkiaSharp is also a good alternative, especially if you also need the drawing bits.
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.
I havent use it a lot but my main issue with the language is 3rd party libraries. MS libraries are great but all the rest seem to be enterprise-milking, poor wrappers or sdk for some cloud service.
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.
You must be joking if you refer to npm packages or ruby gems as examples of good and healthy (!!!) ecosystems.
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.
Respectfully: I’ve used both quite a bit, and have used .NET since the first release.
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.
Nuget did not exist until around .NET Framework 4.0 (which was 14 years ago).
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).
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.
Hey great! Meant no offense. I was talking about what I saw when I tried to start a computervison project in c# like 2 years ago. I am sure there quite a few good libraries.
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.
I also like C# as allrounder language, but I don't think it's about M$-bad rhetoric. In my opinion Java tooling and deployment are still far more enterprise than C# has ever been, especially for huge companies.
C# is very good as a language, have developed in it for 5+ years. The problem is the gap between what MSFT promises to management and actually delivers to developers when it comes to the ecosystem of libraries that you want to use with C#. You really really need to fully read the fine print, think of the omissions in documentation and implement a proof-of-concept that almost implements the full solution to find out the hidden gotchas.
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.
I wonder why they didn't allow Visual Studio to be available for Linux - I wanted to learn C# to satisfy my curiosity and probably could get away by just using VSCode but every tutorial assumes availability of Visual Studio and I use Linux exclusively at home.
Thanks, I've used VS/C# a lot. What are the limitations using VSCode? For instance, how well does it support debugging multiple threads, or designing XAML WPF/Avalonia UI code?
You are ignoring the problem, like the article does. await implies an asynchronous function, but impedance mismatch between two different domains may need that you execute an async-something within a sync-something-else.
in .NET, the problem with that impedance mismatch is IMO very minor. I’ve been programming C# for living for more than a decade, and I can’t remember a single case when I needed to block a current thread waiting for a task to complete.
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.
Microsoft could build a safe, approved low-level way of calling async from sync, maybe with a slight performance penalty, but if they did that, everyone with old crufty legacy code would drop that in and forget about upgrading to async, and they would have to support that method in perpetuity.
That’s what I was thinking. I mean, sure, I’d wish all our code was modern and new, but it’s not. And I can’t just magically make everything async, yet many devs *including MS* think modern code should not offer synchronous APIs. And so I need to call async APIs from synchronous code.
The article continues with all of the different ways you could do sync-over-async and all of them have downsides. There's no "good" solution that could be recommended.
The community consensus is to do `GetAwaiter().GetResult()` and simply accept it's not an ideal usage.
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?
Indeed. Quite often the situation is this: I'm in code that cannot, under any circumstances, be made async. And I need to call an api, that is async and must stay async.
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.
> 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,
> 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.
Unfortunately the solution is very nuanced and boils down to being knowledgeable enough to know when Task.Result won't cause a deadlock, which is outside the scope of this guideline.
Not sure why the downvotes, .Result is avoided because it causes deadlocks, so you have to only use it when you are certain it won't create a deadlock condition. The most common way is to ensure it is already awaited (which defeats the point), however you can also use "var result = Task.Run(() => myTask).Result which will work without deadlocks since you're putting it on the threadpool but you lose the thread context so it won't work for UI tasks. It's not about trying to "undo" what the language developers created, but rather about you need to already be an expert if you want to avoid using await on async methods, which means you wouldn't need to be reading these guidelines in the first place.
I've had this linked in my work's C# guidelines for a while. It's a very useful authoritative source whenever someone decides to do something weird in async land.
FYI this is written by David Fowler, who is personally responsible for the creation of ASP.NET Core, SignalR & Nuget (maybe?).
It's really amazing to me that microsoft, with a headcount of over 200000 people has such small teams (and amazing individuals like Fowler) for their most central technologies. Even Entity Framework has, I think, just a handful of people working on it. Meanwhile, there are thousands upon thousands of people with title "product manager". I suppose the headcounts get vast again for the Azure teams?
On the other hand maybe the success Microsoft has had lately is expressly BECAUSE there are not too many cooks in the kitchen?
David is most likely one of the 10 people who turned the .NET project around and brought it into the modern world. I met him once in a training, a humble, funny guy.
> 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.
The NuGet.Client repo was used for version 3.x and later (a major algorithm and protocol overhaul happened then). But a lot of NuGet code existed before then, e.g.
https://github.com/NuGet/NuGet2/graphs/contributors (Fowler is there with many contributions).
Nothing disingenuous about it. He was on the original team that prototyped and then built NuGet.
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.
I think you might have missed the “not” in my comment. I agree on the numbers but would not second guess whatever happened internally since I have no knowledge of how that worked.
"Async void methods will crash the process if an exception is thrown"
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 always thought that "async void" methods existed so async methods could be sent to event handlers; AND, I've always believed event handlers should have a top-level try/catch.
(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.)
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 believe that ‘crash the runtime’ is legitimate old behavior. IIRC changes eventually made the default crash on UnobservedTaskException an optional configuration setting and there is now an event they are reported with.
> Therefore it is best to go all in, and make everything async at once
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.
> 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.
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 feels frustrating, but it is a good forcing function for the quality of C# projects.
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.
You are clearly thinking about smaller projects. Larger projects can't survive a "stop the world" / "go async" migration. So, what do people do, they use GetAwaiter().GetResult knowing it is bad.
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".
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.
> If a company is not putting a .NET Framework -> .NET upgrade in their near future (which is a great time to go full async)
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.
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>)
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`.
One thing that still surprises me is that C# doesn't have something like a generic async: IE, write one method, and the compiler creates both sync and async versions.
(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) {..}
Sure, you can use it like that, but it's not considered good practice. I think the context missing here is that most modern C# applications are going to be async all of the way down. The entry point can be async. Background service abstractions are async. Your API endpoints in ASP.NET Core are typically async as you're going to be calling some async I/O anyways (database queries, HTTP clients). There's no benefit to writing something in the old callback model when the rest of your code is async/await anyways.
Mapping (spelt "Select" quite often in LINQ) is entirely normal and promoted going forwards. It can optimize extremely well for simple cases. By default it is not parallel and compiles to an underlying for loop.
They're referring to not using .Wait()/.Result on awaitable functions, not about making every function async regardless of whether it needs it. This was a common pitfall that people would do back in the day to avoid having to refactor everything when adding async support to the code, since one await deep in the code could cause half your functions to need async added in a refractor.
> not about making every function async regardless of whether it needs it.
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." ?
If I made mistakes feel free to file an issue or even send me a PR. It's open source! That said, you're right that I don't say how best to call sync from async methods (the latter is more difficult as there's no good way do it well).
Off topic, I wonder if some C# experts here could clarify something for me. If a user wants to, can he have control over allocations, like you can in C++. Answer like I know literally. Nothing about C#
All in all, extremely appalling sentiment and consensus in the comment section.
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?
AsynLocal is an advanced API you rarely deal with.
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
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.