2

This is somewhat related to Why use Async/await all the way down . I have some legacy EF Core I need to maintain with lots of nested calls that are not async. I am wondering if it is just ok to make the top level controller only async and wrap the service call with a Task.Run().

What is the difference between these 2 controller actions. In the first one, EF core is called synchronously. But the call is wrapped in a Task.Run so I assume it will be executed asynchronously

In the second, the EF core calls is async But is it even necessary since the controller action is async? Will async within async within async... give any performance boost over just one async call at the top?

case 1

    //controller, async call
    public async Task<ActionList<string>> GetNames()
    {
        var names = await Task.Run(() => peopleService.GetNames());
        return Ok(names);
    }

//people service layer, no async
public List<string> GetNames()
{
    return _context.People.Where(...).Select(x => x.Name).ToList();
}

case 2

//controller, async
public async Task<ActionList<string>> GetNames()
{
    var names = await  peopleService.GetNames();
    return Ok(names);
}

//people service layer, async
public async Task<List<string>> GetNamesAsync()
{
    return await _context.People.Where(...).Select(x => x.Name).ToListAsync();
}

Soundar Rajan
  • 327
  • 2
  • 11

1 Answers1

4

Task.Run only serves to run your code on the thread pool. This can be useful for WPF or WinForms applications, since it frees up the UI thread to continue processing other UI events. However, it does not bring any performance benefits in ASP.NET, since you're just switching execution from one thread-pool thread to another. When it encounters the EF ToList call, this thread-pool thread will get blocked. If you have a lot of concurrent requests, the thread pool will get exhausted. .NET has a thread injection policy, but this is limited, so some requests will time out. If your load gets very high, you will need so many threads that you will deplete your system resources (CPU and memory).

By contrast, async-all-the-way means that you are never blocking any threads while the database I/O operations are in progress. Each asynchronous operation is handled by the underlying system, and execution only resumes on the thread pool once the operation has completed. This allows your system to scale to a much larger number of concurrent requests.

Douglas
  • 53,759
  • 13
  • 140
  • 188
  • 1
    With database calls you will typically run out of resources on the database server (or hit the protective Connection Pool size limit) long before the size of your .NET thread pool size becomes an issue. So using async database access calls usually doesn't improve the system's overall scalability. Using async might save you 50MB of memory from thread stacks. – David Browne - Microsoft Jun 13 '21 at 16:28
  • 1
    @DavidBrowne-Microsoft: That depends on the architecture of your system. [In an old-school one-database-server architecture, I'd say that's generally true. In systems using cloud databases, that argument is no longer as strong as it once was.](https://learn.microsoft.com/en-us/archive/msdn-magazine/2014/october/async-programming-introduction-to-async-await-on-asp-net#asynchronous-code-is-not-a-silver-bullet) – Stephen Cleary Jun 14 '21 at 10:16
  • No argument, just noting that the overall effect on the scalability of the system is _often_ minimal. – David Browne - Microsoft Jun 14 '21 at 12:05
  • 1
    @DavidBrowne-Microsoft: I wouldn't say that, even for traditional SQL Server backends. The thread pool initially only contains a small number of threads, typically corresponding to the number of cores. The thread pool can inject new threads, but only when a task completes or at 500ms intervals. Thus, if your database operations take longer than 500ms round-trip, you will start experiencing thread pool starvation from as few as 8 or 16 concurrent operations, which is much lower than the scalability limits of SQL Server. – Douglas Jun 14 '21 at 18:07
  • The thread pool will scale up to hundreds of threads if there is a need. Requests will wait for a thread to become available or a new thread, but should not time out. And this is at startup, just like the heaps growing to an eventual steady state. – David Browne - Microsoft Jun 14 '21 at 18:12
  • 1
    @DavidBrowne-Microsoft: .NET only injects one thread every 500ms. If you get a sudden burst of requests, most of them will time out due to thread pool starvation. Also, threads are not lightweight enough to scale to that extent. Each thread needs a 4MB stack on 64-bit systems. Read Microsoft's (old) article on [thread injection](https://learn.microsoft.com/en-us/previous-versions/msp-n-p/ff963549(v=pandp.10)?redirectedfrom=MSDN#thread-injection) or this [more recent analysis](https://mattwarren.org/2017/04/13/The-CLR-Thread-Pool-Thread-Injection-Algorithm/). – Douglas Jun 14 '21 at 19:08
  • So my takeaway is=> it IS good to have async/await all the way down including EF core operations like FindAsync or ListAsync. The response time to a single request may not improve but the system as a whole will be able to serve lot more users since the calls are non-blocking. – Soundar Rajan Jun 16 '21 at 15:31
  • @SoundarRajan: Correct. Asynchronous programming will not improve the response time for requests when the system is under low load, but it will maintain reasonable response times for requests when the system is under high load (whereas blocking calls would suffer from performance degradation under high loads). – Douglas Jun 16 '21 at 17:53