20

Everybody knows that asynchrony gives you "better throughput", "scalability", and more efficient in terms of resources consumption. I also thought this (simplistic) way before doing an experiment below. It basically tells that if we take into account all the overhead for asynchronous code and compare it against properly configured synchronous code it yields little to no performance/throughput/resource consumption advantages.

The question: Does asynchronous code actually perform so much better comparing to the synchronous code with correctly configured thread pool? May be my performance tests are flawed in some dramatic way?

Test setup: Two ASP.NET Web API methods with JMeter trying to call them with 200 threads thread group (30 seconds rump up time).

[HttpGet]
[Route("async")]
public async Task<string> AsyncTest()
{
    await Task.Delay(_delayMs);

    return "ok";
}

[HttpGet]
[Route("sync")]
public string SyncTest()
{
    Thread.Sleep(_delayMs);

    return "ok";
}

Here is response time (log scale). Notice how synchronous code becomes faster when Thread Pool injected enough threads. If we were to set up Thread Pool beforehand (via SetMinThreads) it would outperform async right from the start.

response time

What about resources consumption you would ask. "Thread has big cost in terms of CPU time scheduling, context switching and RAM footprint". Not so fast. Threads scheduling and context switching is efficient. As far as the stack usage goes thread does not instantly consume the RAM, but rather just reserve virtual address space and commit only a tiny fraction which is actually needed.

Let's look at what the data says. Even with bigger amount threads sync version has smaller memory footprint (working set which maps into the physical memory).

stats-1

stats-2

UPDATE. I want to post the results of follow-up experiment which should be more representational since avoids some biases of the first one.

First of all, the results of the first experiment are taken using IIS Express, which is basically dev time server, so I needed to move away from that. Also, considering the feedback I've isolated load generation machine from the server (two Azure VMs in the same network). I've also discovered that some IIS threading limits are from hard to impossible to violate and ended up switching to ASP.NET WebAPI self-hosting to eliminate IIS from the variables as well. Note that memory footprints/CPU times are radically different with this test, please do not compare numbers across the different test runs as setups are totally different (hosting, hardware, machines setup). Additionally, when I moved to another machines and another hosting solution the Thread Pool strategy changed (it is dynamic) and injection rate increased.

Settings: Delay 100ms, 200 JMeter "users", 30 sec ramp-up time.

response-time-2

stats-2-1

stats-2-2

I want to conclude these experiments with the following: Yes, under some specific (more laboratory like) circumstances it's possible to get comparable results for sync vs. async, but in real world cases where workload can not be 100% predictable and workload is uneven we inevitably will hit some kind of threading limits: either server side limits, or Thread Pool grow limits (and bear in mind that thread pool management is automatic mechanism with not always easily predictable properties). Additionally, sync version does have a bigger memory footprint (both working set, and way bigger virtual memory size). As far as CPU consumption is concerned async also wins (CPU time per request metric).

On IIS with default settings the situation is even more dramatic: synchronous version is order(s) of magnitude slower (and smaller throughput) due to quite tight limit on threads count - 20 per CPU.

PS. Do use asynchronous pipelines for IO! [... sigh of relief...]

Eugene D. Gubenkov
  • 5,127
  • 6
  • 39
  • 71
  • I'm not saying that this is "production" scenario, it's synthetic test, but yet valid I think (tests the very essence of sync vs async) and I'm very puzzled by the results and wanted to get some explanation... – Eugene D. Gubenkov Apr 24 '19 at 05:53
  • Look at Task definition `public class Task : IThreadPoolWorkItem, IAsyncResult, IDisposable` so a Task is "simplistically" speaking a background thread. Which might be a pointer to your point that a properly configured synchronous method (I would chose synchronous background method) would have similar characteristics to a Task. However once you take the complexity of this configuration into account, Task would come out to be much superior in most cases. – peeyush singh Apr 24 '19 at 06:01
  • "Everybody knows that asynchrony gives you "better throughput", "scalability", and more efficient in terms of resources consumption." ??? Really? I wouldn't have thought of any of those things. – Enigmativity Apr 24 '19 at 06:04
  • Running multiple *independent* units in a thread pool is a hallmark of asynchronicity. Technically, [neither of your examples are actually synchronous](https://stackoverflow.com/a/748235/5623232) :) – NPras Apr 24 '19 at 06:18
  • IMHO the main advantage of asynchronicity is a flawless and non-blocking UI. You also may improve performance by consuming multiple independent remote microservices, for example, but the thing that users notice first is UX. – Rob Apr 24 '19 at 06:24
  • 1
    I think the load is way too load. Async was a response to C10K problem (or C10M problem in today's reality) – Lesiak Apr 24 '19 at 08:27
  • 4
    I'm prepared to upvote and favourite this question solely for the testing, with evidence, even if the methodology turns out to be flawed. Questions are rarely asked with the premise so well-supported. – Tom W Apr 24 '19 at 08:55
  • *Especially* in today's reality, blocking costs a lot, measured in servers. IO is always asynchronous. Blocking is *emulated* by thge OS. Blocking doesn't mean the thread sleeps, it means it spinwaits (ie 100% core usage) for a time before going to sleep. That's because blocking operations aren't supposed to take a lot of time and resuming a thread is expensive. On a busy site that means you're burning electricity doing nothing. If there are too many requests, you may flood the server doing nothing and probably cause an app pool recycle. – Panagiotis Kanavos Apr 24 '19 at 15:57
  • In other words, async allows more requests to be served by the same hardware by avoiding blocks. On a busy site that translates to fewer *servers* for the same traffic. – Panagiotis Kanavos Apr 24 '19 at 16:01
  • @Panagiotis Kanavos This is precisely what I meant, although a typo could made it unclear (my comment should say the load was way too small) – Lesiak Apr 24 '19 at 16:10

2 Answers2

13

Everybody knows that asynchrony gives you "better throughput", "scalability", and more efficient in terms of resources consumption.

Scalability, yes. Throughput: it depends. Each asynchronous request is slower than the equivalent synchronous request, so you would only see a throughput benefit when scalability comes into play (i.e., there are more requests than threads available).

Does asynchronous code actually perform so much better comparing to the synchronous code with correctly configured thread pool?

Well, the catch there is "correctly configured thread pool". What you're assuming is that you can 1) predict your load, and 2) have a server big enough to handle it using one thread per request. For many (most?) real-world production scenarios, either or both of these are not true.

From my article on async ASP.NET:

Why not just increase the size of the thread pool [instead of using async]? The answer is twofold: Asynchronous code scales both further and faster than blocking thread pool threads.

First, asynchronous code scales further than synchronous code. With more realistic example code, the total scalability of ASP.NET servers (stress tested) showed a multiplicative increase. In other words, an asynchronous server could handle several times the number of continuous requests as a synchronous server (with both thread pools turned up to the maximum for that hardware). However, these experiments (not done by me) were done on a expected "realistic baseline" for average ASP.NET apps. I don't how the same results would carry over to a noop string return.

Second, asynchronous code scales faster than synchronous code. This one is pretty obvious; synchronous code scales fine up to the number of thread pool threads, but then can't scale faster than the thread injection rate. So you get that really slow response to a sudden heavy load, as shown in the beginning of your response time graph.

I think the work you've done is interesting; I am particularly surprised at the memory usage differences (or rather, lack of difference). I'd love to see you work this into a blog post. Recommendations:

  • Use ASP.NET Core for your tests. The old ASP.NET had only a partially-asynchronous pipeline; ASP.NET Core would be necessary for a more "pure" comparison of sync vs async.
  • Don't test locally; there are a lot of caveats when doing that. I'd recommend choosing a VM size (or single-instance Docker container or whatever) and testing in the cloud for repeatability.
  • Also try stress testing in addition to load testing. Keep increasing load until the server is totally overwhelmed, and see how both the async and sync servers respond.

As a final reminder (also from my article):

Bear in mind that asynchronous code does not replace the thread pool. This isn’t thread pool or asynchronous code; it’s thread pool and asynchronous code. Asynchronous code allows your application to make optimum use of the thread pool. It takes the existing thread pool and turns it up to 11.

Stephen Cleary
  • 437,863
  • 77
  • 675
  • 810
  • 1
    Appreciate your thorough answer, Stephen! Honestly, I was waiting your authoritative answer on the topic you are known to contribute a lot. I will try to do some follow-up experiments, in particular, try to push it to the the limits and force more threads (by decreasing the delay). – Eugene D. Gubenkov Apr 24 '19 at 15:06
  • 2
    And don't test from a single source. Sometimes it's the tester and not the tested that gets stressed. – Paulo Morgado Apr 24 '19 at 15:24
1

Trully asynchronous code (I/O) is more scalable because it releases thread pool threads for other work instead of blocking them. So, for the same number of threads being, it can handle more requests.

But it does that at the cost of more control data structures and more work. So, (other than saving thread pool threads) it consumes more resources (memory, CPU).

It's all about availability, not performance.

Paulo Morgado
  • 14,111
  • 3
  • 31
  • 59
  • What I'm trying to demonstrate in the question is that synchronous version does not really show itself as less scalable... `Task.Delay` by the way is "truly asynchronous" – Eugene D. Gubenkov Apr 24 '19 at 12:43
  • 1
    @EugeneD.Gubenkov it's actually far less scalable. Blocking starts with a spinwait, which means a core gets pegged for a while. Blocking a lot of requests results in high CPU usage, which results in frozen servers or app pool recycles. When that happens, the extra traffic can freeze other servers in the farm, resulting in a cascading meltdown. – Panagiotis Kanavos Apr 24 '19 at 16:04
  • 1
    @EugeneD.Gubenkov that's not speculation by the way, that's an "Ask me how I know" and "I told you guys to stop blocking" – Panagiotis Kanavos Apr 24 '19 at 16:06