[HN Gopher] Asynchronous Programming in C#
___________________________________________________________________
Asynchronous Programming in C#
Author : keewee7
Score : 242 points
Date : 2021-09-24 12:33 UTC (10 hours ago)
(HTM) web link (github.com)
(TXT) w3m dump (github.com)
| fhood wrote:
| Am I completely crazy in thinking that we use the terms sync and
| async incorrectly in software?
|
| Synchronous: Simultaneous, at the same time.
|
| Asynchronous: Not Synchronous
|
| So the basic category would be something like serial vs not
| serial where the "not serial" part consists of two approaches,
| synchronous, threads or forks for instance, and asynchronous,
| selectors and callbacks.
|
| Right?...RIGHT?!?! Why do we refer to blocking calls as
| "sync"?!?!
| binarynate wrote:
| I use C# primarily for Unity, and the part about avoiding async
| void caught me off guard. So, I tested it out and found that in
| Unity, throwing an exception from an async void method doesn't
| crash the process. So, it seems that the advice about avoiding
| async void is specific to ASP.NET.
| jayd16 wrote:
| I can confirm this. Unity handles this differently. But this is
| also true of Exceptions in Unity in general.
| D_Guidi wrote:
| unfortunately, the code is simply ugly to read
| SigmundA wrote:
| The sync over async issue is a real common problem for me when
| trying to get a large older codebase converted to async and you
| can't just do it all at once.
|
| You still need support non async callers and you want to share
| code between the new async version and the old sync versions it
| makes it really difficult to do so.
|
| Say you have a db layer you want to move to async but you still
| have to support a sync api over that, no great way to do it
| without hitting the potential issues, instead you have to have
| two versions in the db layer with no great way to share code.
|
| What worse is when you don't even have the option to go async for
| instance if your not on .Net core but Framework 4.8 with the
| latest version of ASP MVC there is no ExecuteResultAsync on a
| action result so you really can't call any async code there
| safely, they added it to MVC core later.
|
| Bottom line sometimes you're at the mercy of your callers and not
| being able to easily expose a sync version of your api when
| needed without a bunch of code duplication is a real problem that
| I have hit. I really think they should have spent more time in
| the beginning to allow that scenario without pitfalls and the
| transition would have been much smoother.
| torginus wrote:
| Database operations should be async, you shouldn't consume them
| synchronously, as they do IO. Async/Await came out in 2012,
| almost a decade ago (and with great first party library
| support, I might add).
|
| Moralizing aside, sometimes you do want to call async APIs as
| synchronous code. I don't think there should be a synchronous
| version of the API implemented as well, you just need to do
|
| _var myValue =
| DoSomethingAsync().ConfigureAwait(false).Result;_
|
| which will avoid deadlocks, and execute your call synchronously
| spaetzleesser wrote:
| "var myValue =
| DoSomethingAsync().ConfigureAwait(false).Result;"
|
| In my view there should be a built-in keyword to do this
| right. It's too easy to get this wrong and even worse
| possible problems only show up rarely.
| jtbarrett wrote:
| If this really needs to happen then I like
| .GetAwaiter().GetResult() instead of .Result to get the
| same exception behavior as await rather than the wrapped
| AggregateException that .Result throws. This is especially
| helpful if DoSomethingAsync sometimes throws synchronously
| rather than returning a task.
| torginus wrote:
| Well, you can put it into an extension method like this:
|
| T SyncResult<T>(this Task<T> task){ return
| task.ConfigureAwait(false).Result; }
|
| Same for ValueTask. It may already be implemented in the
| base .NET library as well.
| gregoryl wrote:
| The reason there isn't a keyword is because its not
| possible to do it right. The example given is far from
| foolproof.
| spaetzleesser wrote:
| But there should be an easy way to do it right. Needing
| to call async functions from non-async code is not
| exactly an unusual thing. Or they need to make absolutely
| everything async which is also problematic, especially in
| terms of raw performance.
| Joker_vD wrote:
| Some things simply don't have an async API at the low level,
| e.g. DNS lookup: there is _no_ asynchronous version of
| getaddrinfo(3). So if you look at the .NET sources, you 'll
| see that Dns.GetHostEntryAsync pushes a task to a thread pool
| that calls getaddrinfo(3).
|
| In the end, you arrive at a "sync top-level APIs -- async
| library APIs -- sync low-level OS APIs" sandwich of dubious
| efficiency.
| [deleted]
| chakkepolja wrote:
| When there's sync OS APIs what's the point of async over
| threads? I thought async APIs use async or polling version
| of syscalls and not blocking ones.
| piedar wrote:
| > var myValue =
| DoSomethingAsync().ConfigureAwait(false).Result;
|
| Came out a decade ago and we still don't know how to use it
| safely.
|
| This example doesn't compile because there is no Result on
| ConfiguredTaskAwaitable. Regardless, ConfigureAwait(false)
| does absolutely nothing here because this Task is not being
| awaited.
|
| If you're going to block this thread, you must push the work
| to another thread or it's going to deadlock when the
| implementation tries to resume a continuation (unless the
| implementation is 100% perfect and the SynchronizationContext
| smiles upon you).
|
| var result = Task.Run(() =>
| CalculateAsync()).GetAwaiter().GetResult();
|
| This avoids the deadlock but can lead to other nasty things
| like thread pool starvation. The only true solution is to go
| async all the way -
| https://blog.stephencleary.com/2012/07/dont-block-on-
| async-c...
| myk9001 wrote:
| >> _var myValue =
| DoSomethingAsync().ConfigureAwait(false).Result;_
|
| Doing this inside ASP.NET request processing code (e.g. a
| controller method) will result in thread pool starvation [1],
| if you see about 50-100 (the numbers are off the top of my
| head, so check for yourself) requests per minute hitting that
| line of code.
|
| P.S.: Sorry for a medium link, but couldn't really find an
| alternative.
|
| [1]: https://medium.com/criteo-engineering/net-threadpool-
| starvat...
| rawling wrote:
| To avoid deadlocks, you don't need the ConfigureAwait(false)
| here, but you do need it to have been applied correctly in
| all the async code you're calling.
| ziml77 wrote:
| There's plenty of database operations I don't do async. We
| heavily make use of ETLs. By their nature those processes are
| very linear and don't benefit at all from being async.
| Roonerelli wrote:
| Is there a particular reason this has been posted today? The last
| commit to it was in April.
|
| It's a good list for sure, just wondering why it's popped up on
| HN
| comeonseriously wrote:
| Would it be different somehow if the last commit was, oh,
| yesterday?
| Roonerelli wrote:
| just wondered if it was relevant to some other discussion I'd
| missed. that's all
| cmpb wrote:
| >Prefer async/await over directly returning Task
|
| This one seems questionable to me. I've never been bitten by any
| of the cons mentioned[1], and it's even noted that doing it this
| way does incur performance costs. I've learned over the years
| that if the code path is very prolific, it pays to avoid the
| async state machine.
|
| I'm curious if others could expand on this one.
|
| [1]
| https://github.com/davidfowl/AspNetCoreDiagnosticScenarios/b...
| Salgat wrote:
| Unless you're doing awaits in a tight loop of
| thousands/millions of calls, the overhead of the state machine
| is almost non-existent, which leads to the next question, what
| are you doing that requires await in a tight loop of that many
| calls? The whole point of await is to use it to yield a thread
| while waiting on a long running operation, if your await
| returns nearly instantly then use the synchronous version and
| avoid the overhead.
| zmj wrote:
| I've run into this in code that completes synchronously in
| the common case, but falls back to an async implementation -
| think caching. The simple way to write this creates a state
| machine even on the synchronous path.
|
| It's possible to work around this efficiently by pulling the
| async code into a separate method and using ValueTask for the
| outer method return type.
| MaXtreeM wrote:
| Always using async/await is recommended to avoid _surprising_
| behavior. If a method with a signature
|
| _Task <Bar> Foo();_
|
| and it is not declared with async and it throws an exception,
| the exception is propagated directly to the call site. Think of
| this usage:
|
| _var getBarTask = Foo();_
|
| _/ / do some other stuff_
|
| _try_ _{_ _var bar = await getBarTask;_ _}_
|
| _catch (Exception ex) { handle exceptions }_
|
| Then if the Foo is not async the exception is thrown at ' _var
| getBarTask = Foo();_ '. If it is declared with async the
| exception is wrapped inside of the Task object and thrown at
| the ' _var bar = await getBarTask;_ '
|
| Yes there is obviously a small performance cost. My guideline
| would be "always use async/await unless you call the method
| hunders or more times a second and the small performace cost
| becomes neglible. And always measure before you optimise.
|
| edit: formating
| ziml77 wrote:
| It's actually the case that's marked async that surprises me
| more. But I don't think the difference has ever mattered in
| code that I've written or worked with.
|
| The reason that the non-async case makes sense to me is that
| I know there's usually going to be some synchronous code
| execution before the function I'm calling has to go async.
| And in that case I expect the code the executed before going
| async to come up the stack where I called it instead of where
| I'm awaiting it. And of course I expect exceptions beyond
| that to only be able to be retrieved when I await the task
| since the call stack will be rooted in the event loop after
| going async.
| rawling wrote:
| This surprised me. When they wrote LINQ and iterables, MS
| went to great lengths to ensure exceptions that could be
| thrown immediately (before iterating) were. I wonder why
| async/await are the opposite.
| jkulubya wrote:
| I don't think I'd have a philosophical problem with
| throwing both synchronously and asynchronously when using
| async/await. After all, the act itself of queuing some work
| with a possible future result does seem like something that
| can fail. But the dotnet team (or c# compiler team, not
| sure) helped devs out by promising not to throw on the
| queuing the work bit when using async/await, and only throw
| at the point where the result should ordinarily be ready.
|
| If you don't use async/await, then I'm not sure how else
| they can help. By returning a task without async, the dev
| claims that they're smart enough to safely kick off some
| async work and possibly provide a result later. But in the
| act of kicking off the work, you break?
| jayd16 wrote:
| There's a couple in here that are the absolute safest things
| even though the alternatives can be done safely. Async void,
| for example, is for dealing with event handlers.
|
| Just like returning Task directly, if you take care with the
| exceptions, you'll have less problems.
|
| Said another way "...unless you know what you're doing" could
| be added to a few of these.
| aowen wrote:
| I used C#'s async/await on a project in 2017, and I took to it. I
| appreciated being able to follow the "relevant" parts of a
| method, without having to jump around to different callbacks.
| That being said, I think I was the only one on the project that
| understood it _well_. Over the course of two years, I learned
| lots of the same gotchas.
|
| Avoid "async void" was one of the catchy mnemonics I learned the
| hard way, because one day our production server crashed because
| it threw an exception in an async void.
|
| I'm working on Java web services now, and it's written using
| synchronous Java servlet framework (Spring/Jetty). My hidden fear
| is that one day we'll discover that our synchronous APIs will
| have to be completely re-written in the async model.
| BoyRobot777 wrote:
| You won't need to rewrite into async model, because project
| Loom will introduce virtual threads. That means your sync code
| will look exactly the same, but will have scalability of async.
| maxpert wrote:
| C# was my first exposure to async/await back in 2015 and I
| initially had trouble wrapping my head around various details
| (i.e. ConfigureAwait etc.). I think the languages that have done
| best job in removing all that detail are Go and Elixir (Beam
| based languages). Which if you pay attention removed the overhead
| of rewiring your brain to do async/await all the way down. I
| repeated async/await systems recently with Kotlin coroutines in
| JVM world and again same problem, this time due to my prior
| knowledge I did hit the ground running but avg Joe had to
| relearn.
| kevingadd wrote:
| IMO ConfigureAwait is just the result of a failure to fully
| consider the implications of various design choices early on.
| To be fair, it is a tough problem, but the ergonomics ended up
| being horrible and the default they chose was probably the
| wrong default.
|
| There are some other choices they made that are arguably not
| the right ones - for example, async code can do some of its
| initial execution on the calling thread and do the rest
| wherever continuations get scheduled (which is configurable...)
| which means you have to have exception handling in two places
| and the way the exception handling works will be different (the
| article calls this out). It is possible to avoid this by having
| the initial call to the async function only create the task but
| not run any of it - of course, there are reasons not to do it,
| performance being one of them, so it makes sense that they did
| it... it's just bad to optimize by default instead of making
| code simpler and more reliable.
|
| My least favorite decision is that inexplicably, async/await
| state machines are very error prone... if any part of your
| codebase accidentally invokes a continuation twice, the state
| machine will potentially begin running twice or even _start
| running again from the beginning with the same local
| variables_. Fixing this would have been as simple as setting a
| bool at the end and checking it at the beginning, but for some
| reason they are dead-set on not fixing it. Premature
| optimization once again.
|
| The existence of 'async void' is also just a complete
| trainwreck. They shouldn't have allowed it, especially since an
| 'async Task' that discards its result is just as easy.
|
| The approach to cancellation (intrusive only) is also
| needlessly complex and gross. Putting a Dispose method on a
| Task would allow consumers of any async API to cleanly signal
| that they no longer need the result of a Task and any
| implementation would be able to observe this without anyone
| having to introduce a new method overload that takes a
| CancellationToken, not to mention that the intrusive
| cancellation design creates extra garbage on the heap. Really
| not obvious to me why they did this instead of reusing 'using
| x' and IDisposable.
| vips7L wrote:
| It's kind of odd that JetBrains chose async/await for Kotlin
| considering the JVM is going towards the Go approach for
| virtual threads. I guess they had to since they wanted to
| support android/js?
| pjmlp wrote:
| The decision happened before Loom came to be.
|
| It is yet another example of impedance mismatch from guest
| languages, when the platform moves into another direction.
|
| The platform language gets the true way, while the guest
| languages get the hard decision how to combine multiple
| approaches, and libraries that only use the new platform
| APIs.
| jayd16 wrote:
| It makes sense for UI patterns and any pattern where you need
| to bind to a specific OS thread.
| SureshG wrote:
| Yeah that decision make sense for multi platform. Suspending
| functions will work on JS and Kotlin Native.
| jayd16 wrote:
| I think it's clear from the languages given that the added
| complexity is to handle UI workflows, no?
| Hercuros wrote:
| You can have UI workflows just as well with a language that
| has (runtime-managed) green threads, like Go and Elixir. It's
| just that you sometimes have to make sure that certain green
| threads have their actions scheduled on the UI (OS) thread
| for backwards compatibility reasons. For example, the Scala
| ZIO library provides the means to control where (on which OS
| thread) code is scheduled in a very intuitive way that does
| not involve ConfigureAwait hacks. ZIO is not green threads
| and rather more like async/await, but a similar approach
| could be implemented in languages with runtime-managed green
| threads as well.
| jayd16 wrote:
| >you sometimes have to make sure that certain green threads
| have their actions scheduled on the UI (OS) thread
|
| But that management is the crux of the issue. The
| complexity of hopping into and out of contexts is what is
| exposed with async/await and coroutine scopes and hidden by
| the more simple syntax.
| gambler wrote:
| Yeah, I don't like Go in most respects, but their approach to
| concurrency is _way_ more intuitive than async /await. That
| said, Go doesn't have any standard promise or futures
| libraries, which is quite ridiculous. Yes, you can roll your
| own or go get one, but that is something basic should be in the
| language.
| Jtsummers wrote:
| As someone who has never really used promises or futures,
| what do they add, or how do they make concurrent programming
| _easier_ or _clearer_ than what 's currently in Go?
| gambler wrote:
| In short, they provide clean API for checking on completion
| and error states of a long-running process from another
| thread without blocking that thread. This is an essential
| pattern for UI-related tasks, including web pages.
| Jtsummers wrote:
| Fair enough, I think we are thinking of futures slightly
| differently. I was thinking primarily about the deferred
| action to get a result (in which case channels and
| goroutines are equivalent with a select to handle,
| perhaps, an error result). You're also thinking of the
| other capabilities around task management and monitoring
| which I was not.
| phamilton wrote:
| An easy example of where having promises is nice:
| a := getFoo() b := getBar() c := getBaz()
|
| If you want to do all 3 in parallel, you may need to use
| channels and wait groups and stuff. In a language with
| promises: const a = getFoo(); const
| b = getBar(); const c = getBaz(); await
| a await b await c
|
| or even better const [a,b,c] = await
| Promise.all([getFoo(), getBar(), getBaz()])
|
| If/when go generics become available, I expect to see
| some libraries that make things easier in golang.
| kevingadd wrote:
| This is built into the .NET API though? You can just
| check the Faulted and Completed properties of the Task
| you're holding.
| Jenk wrote:
| GP wants those to be part of the Go stdlib.
| galaxyLogic wrote:
| Is there a rule as to which methods are best made async and which
| not?
|
| Or, once you start using async would it be best to make ALL
| methods async?
|
| Many methods could be either sync or async. But if you make a
| method that doesn't strictly need to be async async you give
| yourself the option of later making it actually return its result
| after a delay, say reading its answer from the web or
| asynchronously from disk.
|
| Whereas later trying to convert sync-methods to async seems to
| sometimes require big changes to the structure of the whole
| program. If you depend on getting the answer right away there is
| no easy way to modify the code so it in fact returns the answer
| after a delay. Or is there?
|
| A downside to async-methods I can see is that they are harder to
| debug of course.
| ziml77 wrote:
| I recently had to make a change that converted a few functions
| to be async. It was certainly annoying to propagate that back
| in all the signatures, but the biggest issues came with having
| code that was designed under the assumption that the code would
| be synchronous. It was difficult to rework the code in ways
| that would avoid issues with things like the fact that if I use
| a member variable, call one of these functions, and then use
| use that variable again, there's no longer a guarantee that the
| variable still has the same value.
|
| And for issues like that I don't think that having async from
| the start would have helped much. Because if the signature said
| async but everything actually completed synchronously, it's
| possible that people would have been more conscious about those
| issues, but it's also very likely that plenty of cases would be
| missed. Testing wouldn't expose any problems unless the
| implementations were swapped out for code that was actually
| running asynchronously.
|
| It's not easy to call what the right approach is. The best you
| can do is try to guess what the most likely future is and code
| for that. Violating the YAGNI principle can end up adding extra
| work and complexity and even reduced performance for no payoff
| later.
| elboru wrote:
| Another downside is the overhead created. Those "simple" async
| methods are translated into state machine classes under the
| hood. You could probably test performance and see if the value
| you get is worth it.
| wvenable wrote:
| The rule: If you need to call an async method, then you make
| your method also async. As such, your callers will then also
| need to be async.
|
| Visual studio will actually give you a little warning if you
| make a method async and then don't await on any async methods.
| JamesSwift wrote:
| So much content and the `ConfigureAwait` portion, which is the
| BIGGEST gotcha in the whole shebang in my opinion, is not filled
| out?! Especially for Xamarin, you need to understand and use
| ConfigureAwait to properly bounce between UI / background
| threads.
| wvenable wrote:
| What do you mean by this? I consider ConfigureAwait to be more
| of an optimization -- if I want to run something on a UI
| thread, I'm explicit about it.
| Amin699 wrote:
| Using Task.Result or Task.Wait to block wait on an asynchronous
| operation to complete is MUCH worse than calling a truly
| synchronous API to block. This phenomenon is dubbed "Sync over
| async". Here is what happens at a very high level:
|
| An asynchronous operation is kicked off. The calling thread is
| blocked waiting for that operation to complete. When the
| asynchronous operation completes, it unblocks the code waiting on
| that operation. This takes place on another thread. The result is
| that we need to use 2 threads instead of 1 to complete
| synchronous operations. This usually leads to thread-pool
| starvation and results in service outages.
| yummybear wrote:
| This is a great overview
| codeape wrote:
| I hope the author at some point adds the section on
| ConfigureAwait. I've seen code bases where the devs have added
| .ConfigureAwait(false) to all invokations "just to make sure".
| ziml77 wrote:
| In an event loop model, I've never felt the need to reach for
| ConfigureAwait(false). Maybe there's certain operations that
| could be sped up a bit by letting them resume on any thread,
| but generally I want to be sure that the event loop is
| executing my code. There wouldn't be much of a point to using
| an event loop if nothing ever returned back to executing on it.
| rawling wrote:
| That's what you're meant to do!
| maxfurman wrote:
| What's the right way to do throttled async in modern C#? For some
| context, we have a process that needs to make an API call for
| each row in a file - maybe hundreds or thousands. What's the best
| way beyond Wait()'ing for each one to get decent performance
| without DOS'ing the server?
| shhsshs wrote:
| Fire off one task per row, but within each of those tasks use a
| SemaphoreSlim to rate limit your requests to the API.
| older wrote:
| Maybe Bulkhead policy in Polly is a good match here?
| Joker_vD wrote:
| I personally used a semaphore for that. You create a semaphore
| with an initial count of MAX_REQS_PER_SECOND, create
| WORKER_COUNT of looping "worker" tasks that each call
| WaitAsync() on that semaphore before doing request (and _don
| 't_ call Release() after request is done), plus a separate task
| that does either Sleep(100);
| Release(MAX_REQS_PER_SECOND / 10);
|
| or Sleep(1000 * WORKER_COUNT /
| MAX_REQS_PER_SECOND); Release(WORKER_COUNT);
|
| in a loop, depending on what numbers make more sense.
| sheikheddy wrote:
| Can you use MaxDegreeOfParallelism or var throttler = new
| SemaphoreSlim(initialCount: MAX_CALLS)?
| dimaaan wrote:
| Use System.Threading.Channels.
|
| BoundedChannelFullMode.DropNewest, DropOldest, DropWrite, Wait
| specifies the behavior to use when writing to a bounded channel
| that is already full
| throwaway13337 wrote:
| async/await makes things complicated - this is a great
| illustration of some issues with it. Fibers/green
| threads/goroutines seem to be generally easier and not viral.
| C#/dotnet choosing this the async model always bothered me.
| Otherwise, the platform is solid.
|
| It's interesting, therefore, to try to understand why dotnet went
| with the async/await model.
|
| A language maintainer C# talks about the issue here and
| references the rust justification for the same:
|
| https://mail.mozilla.org/pipermail/rust-dev/2013-November/00...
|
| https://github.com/dotnet/runtime/issues/11084
|
| They knowingly seem to have chosen a more complicated model for
| performance. For me, that sounds like a bad trade. Developer time
| is quite a bit more valuable than compute time. The performance
| difference just doesn't seem to justify it.
| _old_dude_ wrote:
| Implementing coroutine requires to change to the code
| generator, the runtime and the garbage collectors and there is
| no general consensus on the way to implement them efficiently
| => this requires a huge investment in engineering.
|
| For Rust, one of the feature of Rust is to have a minimal
| runtime, so using a compiler transformation seems a good fit.
|
| For C#, Microsoft has a limited number of people working on the
| runtime of DotNet. Async/await was developed at the same time
| DotNet was transitioning to DotNet Core which also requires
| massive engineering, so it was about priority. The future will
| tell if at some point coroutine will be added to DotNet.
| jdthedisciple wrote:
| I have been a C# developer for a while as well now and I'm not
| sure if this is true:
|
| "Use of async void in ASP.NET Core applications is ALWAYS bad. "
|
| Depending on the context, it can even be recommended to do async
| void. See Stephen Cleary's brilliant explanation of this:
| https://blog.stephencleary.com/2012/02/async-and-await.html
|
| Edit: The correct link is this, see first table column
| exceptions: https://docs.microsoft.com/en-us/archive/msdn-
| magazine/2013/...
| Avalaxy wrote:
| > it can even be recommended to do async void
|
| I don't see this reflected in the linked article at all. Aren't
| you confusing async void with async Task?
| [deleted]
| fabian2k wrote:
| An exception in an async void function will crash your entire
| ASP.NET Core application. There is no reason at all to use
| these in ASP.NET Core, always use Task or ValueTask as the
| return type of your async functions.
| thrower123 wrote:
| There are still event handlers.
|
| You just have to always remember to always wrap async void
| methods with try { ... } catch (Exception ex){ ...}
| n8cpdx wrote:
| One aspect of C# async/await I don't ever see talked about is the
| ecosystem integration.
|
| Async/await was delivered after the popular .NET UI frameworks
| (WPF, UWP) were designed, and it shows.
|
| Trying to work with data bindings with async is a pain. There are
| things WPF has to make it a bit easier, but UWP doesn't have
| them. A lot of the infrastructure (IValueConverter, for example)
| just won't allow async. There are workarounds, but they are ugly.
| It gets tricky when, as the document mentions, async is viral.
| Constructors and void methods (basically the only options for
| running initialization code when a UI component appears) give you
| not good options for async/await. A lot of the WinRT API (which
| has buggy C# bindings and is markedly unreliable) require async
| for things that were never async in the older implementations. It
| makes cross-platform library development a pain, and exacerbates
| the 'async is viral' issue.
|
| None of what I mentioned is a 'problem' in that it can all be
| worked around and people have been delivering applications with
| such workarounds for a decade. But it is disappointing that
| Microsoft hasn't modernized the UI frameworks to take advantage
| of modern programming patterns.
| ridiculous_fish wrote:
| Why do we need the `async` keyword? What is the difference
| between a function which returns a Task<int>, and an async
| function which returns a Task<int>?
| [deleted]
| tobbebex wrote:
| An non-async Task<int> is returning a Task<int> object, where
| the return statement of an async Task<int> method is of type
| int (which will be wrapped in the resulting Task). "return 1;"
| eg only works in the latter case.
| jayd16 wrote:
| In c# async only enables the await keyword. If await was a
| reserved word from the start it wouldn't be needed.
| bob1029 wrote:
| We use async/await pretty much universally throughout our
| codebase today.
|
| One thing to keep in mind is that this mode of programming is
| actually not the most performant way to handle many problems. It
| is simply the most expedient way to manage I/O and spread trivial
| things across many cores in large, complex codebases. You can
| typically retrofit an existing code pile to be async-capable
| without a whole lot of suffering.
|
| If you are trying to go as fast as possible, then async is not
| what you want at all. Consider that the minimum grain of a
| Task.Delay is 1 millisecond. A millisecond is quite a brutish
| unit when working with a CPU that understands nanoseconds. This
| isn't even a _reliable_ 1 millisecond delay either... There is a
| shitload of context switching and other barbarism that occurs
| when you employ async /await.
|
| If you are seeking millions of serialized items per second, you
| usually just want 1 core to do that for you. Any degree of
| context switching (which is what async/await does for a living)
| is going to chop your serialized throughput numbers
| substantially. You want to batch things up and process them in
| chunks on a single thread that never gets a chance to yield to
| the OS. Only problem with this optimization is that it usually
| means you rewrite from zero, unless you planned for this kind of
| thing in advance.
| torginus wrote:
| async/await is not for CPU-intensive parallelism. I think
| that's pretty much stated in the .NET docs. That's why Parallel
| Compute APIs like Parallel.ForEeach/For are not async. Their
| purpose is to enable non-blocking waits for IO, as well as to
| do stuff like animation on UI where you might want to execute
| procedural code over a larger timeframe.
| MarkSweep wrote:
| The other reason those Parallel methods did use async/await
| is that async/await did not exist in .NET at the time those
| methods were introduced.
|
| But good news! The upcoming .NET 6 release will have a
| Parallel.ForEachAsync method:
|
| https://docs.microsoft.com/dotnet/api/system.threading.tasks.
| ..
| torginus wrote:
| Doing a bit of .NET archeology we find that both Task<T>
| and Parallel.For can be dated to .NET 4.0 So if they wanted
| to, they could've included async/await support. It just
| didn't make sense.
| ridethebike wrote:
| Async/await were officially added to c# 5 and .net
| framework 4.5
|
| I think it was possible to have async/await in .net
| framework 4.0 via some workarounds when it was still in
| CTP mode but I don't recall the details.
| manigandham wrote:
| Yes it is. Async is avoid blocking operations, whether it's
| IO-bound or CPU-bound. There are plenty of cases where
| computation can be offloaded to async tasks (eg: keeping the
| UI responsive).
|
| The official docs even give examples to clarify both
| scenarios: https://docs.microsoft.com/en-
| us/dotnet/csharp/async
| SideburnsOfDoom wrote:
| > This mode of programming is actually not the most performant
| way to handle many problems.
|
| This is correct, it's for increasing _throughput_ in concurrent
| scenarios. Meaning that when your server is processing multiple
| requests at the same time, yielding back rather than busy-
| waiting allows a different request to progress instead (or even
| to start processing a queued request earlier).
|
| When waiting for I/O with another machine (a database, an API,
| etc) you can't wait faster; but you can wait better.
| munificent wrote:
| _> This is correct, it 's for increasing _throughput_ in
| concurrent scenarios._
|
| I believe you mean the exact opposite. It decreases _latency_
| (because task B isn 't blocked waiting for task A to
| complete) but it does so at the expense of _decreased_
| throughput. The context switches add overhead. If you just
| synchronously run A then B, the overall time would be shorter
| (higher throughput) because of less context switching
| overhead.
| SideburnsOfDoom wrote:
| No, I mean that yielding allows more requests to be
| executed at the same time on the same number of threads,
| increasing throughput. Overhead of context switches is not
| that relevant, this is are small fry compared to e.g.
| waiting 100s of milliseconds (or more) for a DB or API.
| Yielding instead of busy-waiting, as I said above, allow
| another request that is ready to execute to do so sooner.
| This leads to higher throughput.
|
| The other reply from reubenbond (
| https://twitter.com/reubenbond ) is correct. Also the
| implication that async does sometimes decrease end-to-end
| latency because you don't have wait for request A to
| complete before starting request B.
|
| async/await is not the same thing as threading, it is about
| using a fraction of a thread: when awaiting, "there is no
| thread" being used.
| https://blog.stephencleary.com/2013/11/there-is-no-
| thread.ht...
|
| > I believe you mean the exact opposite.
|
| As an aside, how about you say what you mean, and I'll work
| on what I mean.
| reubenbond wrote:
| If task A & B perform IO (eg, a DB call) and the
| alternatives are running them sequentially on one thread or
| running them concurrently (via async/await) on one thread,
| then running them concurrently can both decrease end-to-end
| latency and increase throughput.
|
| > If you just synchronously run A then B, the overall time
| would be shorter (higher throughput) because of less
| context switching overhead.
|
| There are no context switches: async/await isn't threads.
| The compiler generates state machines which are scheduled
| on a thread pool. Basically, each time an event happens
| (eg, database request completes or times out, or a new
| request arrives), that state machine is scheduled again so
| that it can observe that event. This doesn't involve
| context switching: you can have 1 thread or N threads
| happily working away on many concurrent tasks without
| needing to context switch between them.
| munificent wrote:
| _> There are no context switches: async /await isn't
| threads._
|
| By "context switch", I didn't meant to imply "hardware
| thread context switch", just the general sense of "spend
| some CPU time messing about with scheduling".
|
| There is overhead to async in that you're unwinding the
| stack, bouncing to the thread pool scheduler, loading
| variables from the heap (since your async code was
| compiled to closures) back onto the stack, etc.
|
| As far as I know, it's always possible to complete some
| given set of work in less total time (i.e. highest
| throughput) using a carefully hand-written multithreaded
| program than it is using async. Of course, most people
| don't have the luxury of writing and maintaining that
| program, so async code can often be a net win to both
| throughput and latency, but the overhead is there.
|
| It's analogous to going from a manually-memory language
| to a language with GC. The GC makes your life easier and
| makes it much easier to write programs that are generally
| efficient, but it _does_ incur some level of runtime
| overhead when compared to a program with optimally
| written manual alloc and free.
| [deleted]
| reubenbond wrote:
| > Consider that the minimum grain of a Task.Delay is 1
| millisecond.
|
| The minimum here is contingent on a few things. The API can
| accept a TimeSpan which can express durations as low as 100ns
| (10M ticks per second: https://docs.microsoft.com/dotnet/api/sy
| stem.timespan.ticksp...). The actual delay is subject to the
| timer frequency, which can be as high as 16ms and depends on
| the OS configuration (eg, see
| https://stackoverflow.com/a/22862989/635314). However, I'm not
| sure how any of this relates to "go[ing] as fast as possible",
| since surely you would simply not use a Task.Delay in that
| case.
|
| > There is a shitload of context switching and other barbarism
| that occurs when you employ async/await.
|
| Async/await reduces context switching over the alternative of
| having one thread per request (i.e, many more OS threads than
| cores) and it (async/await) exhibits the same amount of context
| switching as Goroutines in Go and other M:N schedulers. If
| there is work enqueued to be processed on the thread pool, then
| that work will be processed without yielding back to the OS.
| The .NET Thread Pool dynamically sizes itself depending on the
| workload in an attempt to maximize throughput. If your code is
| not blocking threads during IO, you would ideally end up with 1
| thread per core (you can configure that if you want).
|
| Async/await can introduce overhead, though, so if you're
| writing very high-performance systems, then you may want to
| consider when to use it versus when to use other approaches as
| well as the relevant optimizations which can be implemented.
| I'd recommend people take the simple approach of using
| async/await at the application layer and only change that
| approach if profiling demonstrates that it's becoming a
| performance bottleneck.
| bob1029 wrote:
| > I'd recommend people take the simple approach of using
| async/await at the application layer and only change that
| approach if profiling demonstrates that it's becoming a
| performance bottleneck.
|
| Despite some of the things I presented in my original
| comment, I absolutely agree with this. There are only a few
| extreme cases where async/await simply can't get the job
| done. These edge cases are usually explicitly discovered up
| front. It's rare to accidentally stumble into one of these
| ultra-low-latency problem spaces in most practical business
| applications.
| sparsely wrote:
| Sometimes parallel, rather than async, processing will help
| there, and it's pretty easy in dotnet with .AsParallel()
| thehappypm wrote:
| I don't think anyone really argues that async/await is a raw
| speed win. It introduces overhead, after all. It just makes
| code easier to manage in general, which usually comes with some
| perf tradeoffs.
| manigandham wrote:
| This seems to be conflating several issues. Async just means
| non-blocking. Queue a unit of work (ie: Task) to the runtime
| scheduler and come back to it later.
|
| How you implement that can be with the underlying async/await
| or with your own custom framework. There are many examples like
| Actor frameworks (Akka.net, Microsoft Orleans) or
| System.Channels<> or anything else.
|
| You don't need a rewrite from zero, it's pretty easy to have a
| class with a while(true) loop contained in an async function
| processing things from a System.Channel<> and that will handle
| things on a single thread, while you enqueue work from
| anywhere. You can even use the BackgroundService base class to
| start from: https://docs.microsoft.com/en-
| us/aspnet/core/fundamentals/ho...
| Varriount wrote:
| > and spread trivial things across many cores in large, complex
| codebases
|
| How are tasks spread across cores? My main experience with the
| "await" paradigm is from Python, which is primarily single
| threaded.
| manigandham wrote:
| The .NET runtime has a threadpool with local/software threads
| that share the workload. The Tasks (from async operations)
| are spread across this threadpool, although depending on many
| factors (how quick it finishes, overall load, etc) they might
| just run on the same thread anyway.
| zvrba wrote:
| The QueueProcessor example could have been written using
| channels, which expose async APIs.
|
| Otherwise, as others have commented, I despise how async/await is
| "95% done". The remaining 5% will come to bite haunt you and the
| documentation is less than satisfiyng. E.g., how does
| TaskScheduler interact with async? Nowhere documented, except
| answered on StackOverflow by Stephen Cleary that "it should
| work".
|
| I prefer Java's CompletableFuture and Executors. It's more
| verbose, but at least there's no hidden magic. From the
| documentation you can infer exactly how it'll behave.
| hexo wrote:
| Sorry for OT but whats up with those camelcase method names
| starting with upper case letter? Very weird convention to my eyes
| vizzier wrote:
| Not to get too deep into a bikeshed conversation but I'm pretty
| sure thats is normal c# convention [1].
|
| [1] https://docs.microsoft.com/en-us/dotnet/standard/design-
| guid...
| Philip-J-Fry wrote:
| That's standard C# naming convention.
| Zababa wrote:
| It's called PascalCase or UpperCamelCase.
| anjoz wrote:
| That's what I was looking for
| tester34 wrote:
| http://joeduffyblog.com/2015/11/19/asynchronous-everything/
|
| >We were able to share this experience with .NET in time for C#'s
| await to ship. Sadly, by then, .NET's Task had already been made
| a class. Since .NET requires async method return types to be
| Tasks, they cannot be zero-allocation unless you go out of your
| way to use clumsy patterns like caching singleton Task objects.
| kryptiskt wrote:
| .NET Core and later has ValueTask for that usecase.
| spaetzleesser wrote:
| Around 50% of my work .NET coding and I find It's becoming
| really hard to keep up with this. .NET more and more feels to
| me like the typical MS approach where they just keep cranking
| out new stuff without cleaning up existing stuff. Some of the
| new things are very good, some are half baked, and it's
| difficult to figure out on what side these new features are.
|
| Just lately I did some Entity Framework coding and noticed
| that some things are async compatible, but others aren't, so
| you end up doing a lot of strategizing coding around these
| omissions and creating questionable workarounds.
|
| I really wish they would go back to the drawing board and
| simplify things. Same could be said for their various XAML
| dialects. It's just too damn verbose.
| thrower123 wrote:
| The problem is that .NET has been taken over by web
| developers, and they expect the kind of breakneck pace of
| change and half-baked tools that the Javascript ecosystem
| has become accustomed to.
| datenarsch wrote:
| This is so true. .NET has been steadily going downhill
| for some time now. The best indicator is the absolutely
| rotten documentation for the more recent .NET stuff.
| Compare that to the older .NET Framework and/or Winapi
| documentation which was excellent.
| oaiey wrote:
| It is fighting in a space which is very competitive.
| Java, Go, JavaScript and Python. The later two are being
| favorite of every university or coding camp graduate. You
| stay relevant or you die.
|
| Unfortunately, that implies faster dev cycles and areas
| like docs which are not well served.
| Zababa wrote:
| How is .NET fighting against JavaScript or Python? Java
| or Go, I can understand but not JavaScript or Python.
| Jenk wrote:
| Serverless computing is dominated by node, go, and
| python. .NET just doesn't startup fast enough.
| Zababa wrote:
| Good point, I didn't think about that.
| spaetzleesser wrote:
| Agreed about documentation. They produce a lot of it but
| it's hard to use and doesn't really give you the big
| picture. I know I am old but in the 90s and 2000s the
| MSDN documentation was fantastic. Sad to see it going
| downhill that much.
| tester34 wrote:
| but shouldnt it be that ValueTask is used everywhere by
| default and class "exists for some usecase"?
| jayd16 wrote:
| It kind of depends. Class is safer because you can have
| multiple calls to await and the TPL was designed to be
| mostly as safe as possible by default....but yeah, it does
| hurt that its not alloc free.
| oaiey wrote:
| Should: yes. Can: no. Why: backward compatibility
| dpix wrote:
| Been a few years now since I've worked much in C# but these all
| seem like things that should be linting rules.
|
| Can these be added as warnings to the compiler? Can you have
| custom lint/compiler warnings from the community like eslint?
| dustinmoris wrote:
| I would call myself an extremely experienced and knowledgable C#
| programmer with 10+ years of experience and even I found a few
| things surprising or new in this guide. I think C# async/await
| implementation is the biggest con on the .NET community, because
| it gets constantly hailed as one of the easiest ways of async
| programming but this guide itself proves to me that there are so
| many gotchas which are not obvious at all that it's actually not
| that easy after all. When I compare this with goroutines I do
| sometimes wish .NET would have a Go like model instead.
| shikharraje wrote:
| Ex-.NET guy here, can confirm. Coming from JVM-land I was
| constantly told that async await is something that makes
| C#/.NET much better than Java. I personally could not
| understand why. Async await is not as easy as it looks, and
| most .NET programmers who I knew, would just hammer at things
| to make it work.
|
| "Hey, this is an HTTPClient call? Put an await in front of it?"
| "Oh, is the IDE showing an error? Try .ConfigureAwait(false)?"
| "Oh, still some issue? Try putting async in the method
| declaration?" "Still showing an error? Remove the
| .ConfigureAwait() and just try async?"
|
| At some point, Visual Studio would stop showing warnings and
| errors, and then the code would pass review.
|
| Go is better in the sense that, at least people _understand_
| what a goroutine is and how /when to use it correctly.
| Hawxy wrote:
| > most .NET programmers who I knew, would just hammer at
| things to make it work.
|
| I hope the examples you listed are facetious or from the very
| early days of async/await in C#, otherwise I'd seriously
| question the skillset of the supposed .NET programmers.
|
| Visual Studio is fairly good at handling incorrect use of
| async/await, and in all of the examples you listed, the
| actual solution should've been "read the IDE error, hit the
| bulb and apply the automatically suggested fix", not "ignore
| the IDE error and smash keyboard until it works".
|
| ConfigureAwait usage is also not something you'll usually see
| outside of library code in modern C#.
| thrower123 wrote:
| async/await is so much wildly better than previous version of
| .NET async programming.
|
| If you've ever had to deal with the IAsyncResult pattern in
| older .NET code, you'll never complain about await.
|
| https://docs.microsoft.com/en-
| us/dotnet/standard/asynchronou...
| mmarq wrote:
| Depending on the task, I find C# async/await more intuitive and
| complete than Go.
|
| I haven't used Go for a while, but you can't await a goroutine,
| you have to use a channel, which is more complicated than just
| using 'await'. C# has channels, so you can replicate Go's
| model.
| gameswithgo wrote:
| You can get closer to the Go experience if you just ignore all
| the tuning knobs and not try to optimize performance. .NET 6 is
| reducing the penalty of some of the gotchas too, so things are
| getting better.
| [deleted]
| lazulicurio wrote:
| The way I put it: .NET async makes the easy things easier and
| the hard things harder.
|
| The problem is that Task/Task<T> was the foundation for async,
| and it's a bad foundation. Even with the ability to write your
| own duck-typed awaiters (and the advent of ValueTask), the
| widespread use of Task means if you're writing async code
| you're going to have a tough time getting away from it.
| UglyToad wrote:
| I think this is the crux of the matter. Since Task and the
| TPL predated async, iirc, people get befuddled by the
| parallelism Vs concurrency (if that's the correct term) parts
| of the Task API.
|
| Certainly the async story is a lot more complicated in
| desktop but it is very simple for most server scenarios,
| simply put "use this async call so that the thread can do
| other things while you wait for the db to respond" and the
| model in code is much preferable to callback hell.
| lazulicurio wrote:
| IMO it's more fundamental than the parallelism v
| concurrency split.
|
| Microsoft in general has a tendency to bolt on
| functionality in a kinda slapdash manner when another team
| wants it, so you get a lot of cruft that really doesn't
| belong in the Task class[1] but is there because someone
| wanted a way to handle their special case so it just got
| thrown into Task.
|
| [1] See https://source.dot.net/#System.Private.CoreLib/Task
| .cs,045a7...
| geewee wrote:
| Agree as well. Not necessarily on the pros/cons list, but
| there's definitely a few gotchas here I didn't know about.
| horsawlarway wrote:
| Strongly agree.
|
| Honestly, just the first point "Asynchrony is viral" is a huge
| fucking flag that this implementation sucks.
|
| It doesn't need to be viral, they just needed to make passing a
| continuation _easy_ , and they failed miserably.
|
| Overall - I really like most of C#, but that async/await
| implementation is poor at best.
| torginus wrote:
| I thought about this for a while as well, especially as I'm
| both a Go and .NET programmer, and made the following
| observation: Go and .NET have something that's viral about
| their IO code. In Go's case it's errors, in .NET's case it's
| async. Then I realised that basically all code that is async
| in .NET is I/O code just like all code in Go that throws
| runtime errors is I/O code as well. This is not a perfect
| heuristic, but it works 95% of the time.
|
| Which means that .NET code tends to suffer from the same
| issue as Go code. The solution is the same as well: Separate
| out the code that does logic processing from the code that
| does I/O. This way only a few top level functions will become
| async. I find that this makes my code cleaner and more
| testable as well.
| fabian2k wrote:
| I don't think there are really ways to do async programming
| that don't have gotchas. It's just inherently complicated.
|
| And of course C#/.NET is a bit older and also very large, it
| has more surface area for weird behaviour that might not be
| easily fixable due to backwards compatibility.
|
| Things like using void as a return type for an async function
| can be a nasty surprise if you're new, but this is not an issue
| at all once you know this (or if you simply used the right
| examples and used Task from the start). It's not a subtle
| gotcha.
|
| Sync over async is much sneakier and can be very nasty. But I'm
| not sure you can avoid this when interacting with a
| language/environment that used to be mostly sync and switched
| to async. If everything is async you don't have this issue, so
| this is better for newer codebases.
| only_as_i_fall wrote:
| Sure, but I'd argue that the best way to handle this is to
| just stick to the already existing paradigms that developers
| have experience with. It's not like there are any problems
| which can't be solved without async or tpl, and it's not like
| Microsoft wants to introduce a similar paradigm outside of
| the .net ecosystem, so it's just creating an artificial
| barrier and making the code harder to understand so that they
| can post clean looking examples on their doc pages.
| contextfree wrote:
| JavaScript/TypeScript and C++ do implement a similar
| paradigm, loosely patterned on what C# does?
| ziml77 wrote:
| Python and Rust as well. C# invented a large part of the
| design of async/await that has been adopted by other
| languages. I figure if there's a better way, it would
| have been improved on by now since those other languages
| had plenty of time to see the issues with C#'s version.
| metaltyphoon wrote:
| Most claimed "simpler" languages are only being used for
| backend systems, i.e go, BEAM languages. There's a reason
| that any language where UI needs to be considered has
| used what C# has. The flow of code is simplified because
| you don't want to be on callback hell.
| only_as_i_fall wrote:
| Strong agree. I've seen devs with 20 years of experience on me
| write silly inefficient code because they're lulled into a
| false sense of security by the marketing of async/await.
|
| Multithreading is one of the hardest problems in software, and
| Microsoft decided that the best way to solve it is to get smart
| and experienced people to stop forget everything they know and
| instead learn a bunch of opaque apis that interact with an
| incredibly complex internal state machine.
|
| It hardly seems worthwhile to me.
| nobodyandproud wrote:
| async/await has its place, but your application needs to be
| designed for it.
|
| The most illuminating moment for me was when I realized that
| there is no multi-threading involved with pure async/await.
| bobcostas55 wrote:
| async/await is not about multithreading at all though...
| only_as_i_fall wrote:
| In a vacuous sense, but in practice you almost always use
| asynchronous code to achieve concurrency.
|
| The canonical Microsoft tutorial on async spends about half
| its time talking about hiw to make your code concurrent to
| take advantage of async.
|
| https://docs.microsoft.com/en-
| us/dotnet/csharp/programming-g...
| nightski wrote:
| Concurrency does not require multi-threading. Maybe you
| mean parallelism? Concurrency can still be really
| valuable in the context of a single threaded application.
| only_as_i_fall wrote:
| I did mean parallelism, but I think the point stands.
| There's very little practical use of async await outside
| of multithreading.
| joconde wrote:
| If a program wants to perform a task in an async way
| without delegating it to an external program (like a
| database server or the OS' I/O system), it has to use
| threads, right? I think the point is that concurrency,
| for some tasks, basically requires multithreading. Not
| for the parallelism benefits, but just to be able to make
| concurrency possible for a task that requires blocking a
| thread.
| nightski wrote:
| Concurrency is more about having order independent units
| of computation. You can concurrently run operations on a
| single thread, although there is less benefit if no IO is
| involved. It's not something you'd likely do in practice
| unless there was IO.
| spaetzleesser wrote:
| " many gotchas which are not obvious at all that it's actually
| not that easy after all"
|
| Totally agree. At first look async/await is simple and
| straightforward but it's way to easy to mess up in subtle ways.
| Most people don't even notice that their code has problems
| until they get weird behavior in production.
|
| In general I believe they made async way too pervasive in the
| framework and are also inconsistent.
| zwieback wrote:
| Agree but it just highlights that any kind of programming with
| more than one linear path of execution is hard. Before
| async/await, coroutines, etc. we all had to learn that the hard
| way. It's helpful to know what a process, thread or lightweight
| thread in your system is and at what cost it comes. The cost
| and frequency of context switches is not something you can
| ignore and will probably be forced to profile at some point,
| hopefully sooner than later.
|
| With these newer programming models there are easier ways to
| distribute work but unless you really dig deep and understand
| the basic mechanisms you will be lulled into a false sense of
| security.
|
| When async was first added to .Net I read through the details
| of how boldly the compiler re-writes my code and I was a bit
| shocked, like, can it really do that? Now I always keep that in
| mind as soon as I start typing a...
___________________________________________________________________
(page generated 2021-09-24 23:01 UTC)