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.
> 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/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.
> 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.
Honestly if you're in the situation where it comes down to individual CPU clock cycles I can't imagine C# (or similar Java, Go, etc.) being useful at that point. Too much going on that's not in the view of the developer.
Some of the highest throughput systems on earth are written in either Java or C#.
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.
> 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.
> 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.
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.
> 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.
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 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.
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.
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).
There's Task.Yield() which yields instantly. Task continuations happen on the same core by default, until you hit an IO completion or something else that knocks it onto the thread pool. This means that chaining lots of awaits together is very efficient, at least until you hit something that forces you to actually sleep.
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.
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...
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.
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.
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.