Asynchronous Programming in C#
github.com
github.com
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.
But good news! The upcoming .NET 6 release will have a Parallel.ForEachAsync method:
https://docs.microsoft.com/dotnet/api/system.threading.tasks...
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.
The official docs even give examples to clarify both scenarios: https://docs.microsoft.com/en-us/dotnet/csharp/async
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.
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.
> 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.
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.
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.
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/system.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.
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.
Check out the LMAX disruptor sometime. Throughput rates measured in hundreds of millions of serialized events per second are feasible in these languages if you are clever with how you do things.
How are tasks spread across cores? My main experience with the "await" paradigm is from Python, which is primarily single threaded.
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...
In practice I tend to use a lot of homemade TaskCompletionSource, explicit threading and interlocked stuff where I need more control of continuation.
There's also a downside to explicit synchronization which you don't mention - if you design your threading for one load pattern, and your actual load is a different pattern, it crushes your application and it's difficult to refactor.
For instance, if you expect few users and many requests you might have a thread per user with a work queue for their requests. If you have many users with few requests then you have thousands of threads, which are actually context switches unlike Task yields.
I've heard that Midori was 50% faster than Windows, and it was nearly entirely written in something like C# with something like Tasks. The runtime was extremely different (no virtual memory, no threads) but it proves that the model can outperform traditional OS threading.
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.
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.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.
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.
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...
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.
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
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?
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.
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.
Most documentation -- and most code I've seen in the wild -- reads like this:
var foo = await GetFooAsync(...);
var bar = await GetBarAsync(...);
var baz = await GetBazAsync(...);
The timeline of that code is exactly same as the standard synchronous version, just with extra steps and pauses.The following version is more verbose -- which makes it feel slower -- but can provide dramatic speed ups even for a single user by overlapping requests so that they run concurrently:
var fooTask = GetFooAsync(...);
var barTask = GetBarAsync(...);
var bazTask = GetBazAsync(...);
var foo = await fooTask;
var bar = await barTask;
var baz = await bazTask;
Unfortunately, I've literally never seen this design pattern in the field...In JS this could be...
const [
fooTask,
barTask,
bazTask,
] = await Promise.all([
GetFooAsync(...),
GetBarAsync(...),
GetBazAsync(...)
]);
... PS in your code above you assign GetBazAsync to bar and baz. :-)I believe this kind of copy-paste "last line effect" one of the most common errors in programming: https://hownot2code.com/2016/08/15/the-last-line-effect-typo...
Task.WaitAll is the C# equivalent: https://docs.microsoft.com/en-us/dotnet/api/system.threading...
But it's not necessarily faster, there are corner cases where waiting for all tasks prevents some concurrent computations (e.g.: JSON parsing) from occurring.
i.e. you have to write
var (fooTask, barTask, bazTask) = (GetFooAsync(), GetBarAsync(), GetBazAsync());
await Task.WhenAll(fooTask, barTask, bazTask);
var (foo, bar, baz) = (fooTask.Result, barTask.Result, bazTask.Result);
It's possible to write a custom awaiter extension method that allows awaiting tuples of tasks, so once that's in place you can just write var (foo, bar, baz) = await (GetFooAsync(), GetBarAsync(), GetBazAsync());
There are third-party packages that do this for you and it's reasonably easy to write yourself if you understand the inner workings of async/await, but it's not part of the standard library.var bar = await GetBarAsync(...);
var baz = await GetBazAsync(...);
Unfortunately this is the kind of example that is being used in the MS docs and elsewhere to explain async. I spent a long time figuring out what the difference was when they ran the exact same way as the synchronous version.
The other use-case was to run multi-step operations which involve waiting on UI threads of appplications, which wouldn't have worked with blocking waits (would prevent redraw). For that use-case "speed" also isn't the highest priority.
For naive async code, there is a surprisingly narrow range of loads where this is true: only something like 80-99% load. Any higher and latencies start to go towards the stratosphere, or memory usage grows exponentially.
Of course, this is fixable with the appropriate use of backpressure and timeout cancellations, but I've never seen this implemented correctly and consistently anywhere. Almost all web apps in the wild fall over when load goes from 100% to 101%. They don't become 1% slower! Instead they take 30s to return a page or just start spewing 5xx errors.
For a point of comparison, Java is abandoning the complex and fragile async approach in favour of user-mode scheduled lightweight threads, which are vaguely similar in terms of efficiency, but are much easier for programmers to understand. They're also compatible with traditional threaded code.
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.
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.
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
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.
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.
In the end, you arrive at a "sync top-level APIs -- async library APIs -- sync low-level OS APIs" sandwich of dubious efficiency.
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...
It does help if there is a SynchronisationContext active, like in legacy ASP.NET
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...
>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.
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.
Unfortunately, that implies faster dev cycles and areas like docs which are not well served.
FWIW, github issues for all things .net have been a surprisingly good resource. Especially on the hot new things, microsoft folks are very responsive and helpful. I dare say it's better for many topics than stackoverflow.
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.
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.
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.
Could you expound upon this? Not sure I understand the context, but would like to. Are you assigning a value to a member variable by awaiting the return of an async method, not clear would cause you to lose the value.
Part of the issue is that 'sync' is the default. I wonder if it would be better the other way. Because sync is the default it is easy to "simply" write sync methods in cases where async might be a better choice, and, that decisions is often hard to reverse later.
I'm working with JavaScript but I assume the issues and questions are similar as with c#.
I think the question could be rephrased as "When should I NOT use async methods?"
Visual studio will actually give you a little warning if you make a method async and then don't await on any async methods.
If your computation code reach out to fetch data or trigger side-effects I’d take that as a sign of it being badly factored. Try to push the async parts up the stack to an orchestration layer, and keep the computation code “pure”
So I'm not sure if it's always clear what should be the perfect factoring.
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.BoundedChannelFullMode.DropNewest, DropOldest, DropWrite, Wait specifies the behavior to use when writing to a bounded channel that is already full
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.
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.
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.
On the server end, I strongly agree with you that for 95% of applications a threaded environment - and even using plain OS threads - would likely be easier and fast enough. But I guess everyone also wants to support the remaining 5% of applications, like "build a 100k clients proxy server".
Btw: Fibers can also be viral. E.g. if they are multiplexed on a single OS thread (non work-stealing scheduler) you still can't block in them, and need fiber-aware methods of everything. If you use a work-stealing scheduler then methods which use thread-local storage might be subject to undefined behavior, because the currrent thread might change inside the execution of the function at an invisible yield point.
Can these be added as warnings to the compiler? Can you have custom lint/compiler warnings from the community like eslint?
They are called “analyzers” in .net though
"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/...
I don't see this reflected in the linked article at all. Aren't you confusing async void with async Task?
You just have to always remember to always wrap async void methods with try { ... } catch (Exception ex){ ...}
There are two concepts:
1) _awaiters_ offer methods that the compiler-generated code will call to schedule continuations and ask whether it is completed. The thing that you call `await` on needs to offer a `GetAwaiter()` method that returns such an awaiter. (Due to the nature of the duck typing it might also be an extension method actually, so you can make types in other assemblies retrospectively awaitable)
2) _async method builders_ offer methods to perform the state machine transitions and connect them to the result object (which is traditionally of type `Task<T>` or `Task`). To register other types you can decorate them with the `System.Runtime.CompilerServices.AsyncMethodBuilderAttribute` attribute to tell the compiler what builder to use depending on the type you want to return in your async method.
I recommend this blog post series by Sergey Teplyakov for more details: <https://devblogs.microsoft.com/premier-developer/dissecting-...>
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"?!?!
[1] https://docs.microsoft.com/en-us/dotnet/standard/design-guid...
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.
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.
The most illuminating moment for me was when I realized that there is no multi-threading involved with pure async/await.
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...
"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.
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...
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#.
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.
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.
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.
This is basically what some language communities are trying to capture with “monads” (like “the IO monad”)
There are some work yet on how to make such representations compose[1] (like how IEnumerable + Task = IAsyncEnumerable) but eventually we’ll probably see some form of effect systems for all such things reach mainstream languages.
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...
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.
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.
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.
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...
It's a good list for sure, just wondering why it's popped up on HN