9 ms·
Asynchronous Programming in C#
- neonsunset 2y agoImportant: following #prefer-asyncawait-over-directly-returning-task from https://github.com/davidfowl/AspNetCoreDiagnosticScenarios/blob/master/AsyncGuidance.md#prefer-asyncawait-over-directly-returning-task https://github.com/davidfowl/AspNetCoreDiagnosticScenarios/b... is not correct. 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>)
- jddj 2y agoThere are a lot of points in that readme and not so many in this comment. Can you justify further?
- neonsunset 2y agoYes. 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`.
- jddj 2y ago> 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.
- romanovcode 2y agoOfftopic 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 :(
- osigurdson 2y ago> M$-bad rhetoric If M$ is bad, Oracle is Beelzebub.
- sandreas 2y agoI 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.
- papichulo2023 2y agoI 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.
- highwaylights 2y agoThis to me is the biggest drawback. 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.
- bertylicious 2y ago"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.
- tyleo 2y agoI 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.
- Netcob 2y agoThat's my understanding too. 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.
- deleted 2y ago[deleted]
- gwbas1c 2y agoI 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.)
- pif 2y ago> There are very few ways to use Task.Result and Task.Wait correctly so the general advice is to completely avoid using them in your code. 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.
- pif 2y agoTwo people already downvoted without replying. Tell me you have never developed complex software without actually telling me!
- mrkeen 2y agoI didn't down-vote. > 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,
- deleted 2y ago[deleted]
- pif 2y ago> 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.
- alkonaut 2y agoIndeed. 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.
- Hawxy 2y agoI'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?).
- throwaway2990 2y ago[dead]
- Kuinox 2y agoAre you sure for nuget? I never saw Fowler being credited for nuget until now.
- leosanchez 2y agoIIRC he worked in nuget. Not sure if he created it.
- wordofx 2y agoDavid has never worked on Nuget. He created SignalR and had been a huge influence on many internal parts of .NET.
- Hawxy 2y agoIs "creator of nuget" in his twitter bio just an inside joke?
- mrkeen 2y ago> Asynchrony is viral > 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) {..}
- Salgat 2y agoThey'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.
- mrkeen 2y ago> 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." ?
- caleblloyd 2y agoI 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.
- HumblyTossed 2y agoIt’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.
- uticus 2y agoC# 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.
- neonsunset 2y agoGo 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).
- osigurdson 2y ago> 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.
- hnthrow289570 2y agoThis 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.
- osigurdson 2y agoYou 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".
- hnthrow289570 2y ago>You are clearly thinking about smaller projects. 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.
- deleted 2y ago[deleted]
- davidelettieri 2y agoThere is an analyzer https://www.nuget.org/packages/Microsoft.VisualStudio.Threading.Analyzers https://www.nuget.org/packages/Microsoft.VisualStudio.Thread... to catch some common mistakes
- emorning3 2y agoThank you for pointing that out. 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.
- jiggawatts 2y agoSomething 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.
- Atotalnoob 2y agoAt that point, isn’t Task.WhenAll() better?
- jiggawatts 2y agoOnly if the return types are identical.
- petrzjunior 2y agoIf 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.
- jiggawatts 2y agoHere’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.
- neonsunset 2y agoAll 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#.
- mrkeen 2y agoI'll grant you it is weird. 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?
- neonsunset 2y agoAsynLocal 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
- gwbas1c 2y agoOne 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.)
- lfpeb8b45ez 2y agoMy understanding is that the .NET team is working toward this with Interceptors: https://github.com/dotnet/roslyn/blob/main/docs/features/interceptors.md https://github.com/dotnet/roslyn/blob/main/docs/features/int...
- hokkos 2y agoSadly the ConfigureAwait section is empty, and one of the most confusing part.
- bdjsiqoocwk 2y agoOff 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#