Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Async processing in asp.net core and kestrel thread pool

I'm new to ASP.NET Core and to C# in general coming from the Java world and I'm a bit confused about how async/await keywords work: This article explains that if a method is marked as async it means that

"this method contains control flow that involves awaiting asynchronous operations and will therefore be rewritten by the compiler into continuation passing style to ensure that the asynchronous operations can resume this method at the right spot.” The whole point of async methods it that you stay on the current thread as much as possible

So I thought that it is a very cool way of passing control between an async method and its caller running on the same thread implemented on byte-code/virtual machine level. I mean I expected that the below code

public static void Main(string[] args)
{
    int delay = 10;
    var task = doSthAsync(delay);
    System.Console.WriteLine("main 1: " + Thread.CurrentThread.ManagedThreadId);
    // some synchronous processing longer than delay:
    Thread.Sleep(2 * delay)
    System.Console.WriteLine("main 2: " + Thread.CurrentThread.ManagedThreadId);
    task.Wait();
}

public static async Task<String> doSthAsync(int delay)
{
    System.Console.WriteLine("async 1: " + Thread.CurrentThread.ManagedThreadId);
    await Task.Delay(delay);
    System.Console.WriteLine("async 2: " + Thread.CurrentThread.ManagedThreadId);
    return "done";
}

would write

async 1: 1
main 1: 1
main 2: 1
async 2: 1

by passing control from doSthAsync to Main with await keyword and then back to doSthAsync with task.Wait() (and all of this happening on a single thread).

However the code above in fact prints

async 1: 1
main 1: 1
async 2: 4
main 2: 1

which means that it is merely a way to delegate an "async" method to a separate thread if the current thread is busy, which is almost exactly opposite to what the mentioned article states:

The “async” modifier on the method does not mean “this method is automatically scheduled to run on a worker thread asynchronously”

So since "continuations" of async methods apparently are or at least may be scheduled to run on a different thread, I have some questions that seem fundamental for C# apps scalability:

Does each async method may run on a newly spawned thread or is the C# runtime maintaining a special thread-pool for this purpose and in the latter case how do I control the size of this pool? UPDATE: thanks to @Servy I now know that in case of CMD-line apps it will be a thread-pool thread, but I still don't know how to control the number of these thread-pool threads).

How about in case of ASP.NET Core: how can I control the size of a thread-pool that Kestrel uses to perform async entity framework operations or other (network) I/O? Is it the same thread-pool it uses to accept HTTP requests?
UPDATE: thanks to this and this article by Stephen Cleary I now understand that it will run "kind-of-single-threadly" by default, ie:, at any given time there will be exactly one thread within the SynchronizationContext of a given HTTP request, but this thread may be switched to another when some async operation is marked as finished and task is resumed. I also learned that it is possible to dispatch any async method "out of a given SynchronizationContext" to a thread-pool thread by using ConfigureAwait(false). Still, I don't know how to control the number of thread-pool threads and if it is the same pool that is used by Kestrel to accept HTTP requests.

As I understand if all I/O is done asynchronously properly then the the size of these pools (whether it's just 1 thread-pool in fact or 2 separate) does not have anything to do with the number of outgoing connections being opened to external resources like DB, right? (threads running an asynchronous code will be continuously accepting new HTTP requests and as a result of their processing will be opening outgoing connections (to DB or to some web server to fetch some resource from the web) as fast as they can without any blocking)
This in turn means that if I don't want to kill my DB I should set a sane limit of its connection pool, and make sure that awaiting for an available connection is also done asynchronously, right?
Assuming that I'm correct I would still want to be able to limit the number of thread-pool threads in case some long lasting I/O operation is mistakenly done synchronously to prevent large number of threads being spawned as a result of large numbers of threads being blocked on this synchronous operation (ie: I think it's better to limit performance of my app rather than make it spawn insane number of threads due to such bug)

I also have "just out of curiosity" question: why actually async/await is not implemented to execute async tasks on the same thread? I don't know how to implement it on windows but on Unix this could be implemented using either select/poll system calls or signals, so I believe that there's a way to achieve this on windows as well and it would be a really cool language feature indeed.
UPDATE: if I understand correctly this is almost how things will work if my SynchronizationContext is not null: ie code will run "kind-of-single-threadly": at any given time there will be exactly one thread within a given SynchronizationContext. However the way it is implemented will make a synchronous method waiting for async task to deadlock, right? (runtime will schedule marking of the task as finished to run on the same thread that is waiting for this task to be finished)

Thanks!

like image 467
morgwai Avatar asked Dec 12 '16 13:12

morgwai


2 Answers

Worth to mention here, as the question is with regards to dot net Core, that unlike .Net there isn't a SynchronizationContext in dot net core

your async Task will be run using any thread for the thread pool.

ConfigureAwait isn't relevant, unless you think it will be used in .Net

see https://blog.stephencleary.com/2017/03/aspnetcore-synchronization-context.html

and ConfigureAwait(false) relevant in ASP.NET Core?

like image 191
AmitE Avatar answered Sep 30 '22 03:09

AmitE


await uses the value of SynchronizationContext.Current to schedule the continuations. In a console app there is no current synchronization context by default; so it's just going to use thread pool threads to schedule the continuations. In applications with a message loop, such as winforms, WPF, or winphone applications the message loop will set the current sync context to one that will send all messages to the message loop, thereby running them on the UI thread.

In ASP applications there will also be a current synchronization context, but it's not a particular thread. Rather, when it's time to run the next message for the sync context it takes a thread pool thread, sets it up with the request data for the right request, and then has it run the message. This means that when using the sync context in an ASP application you know that there is never more than one operation running at a time, but it's not necessarily a single thread handling every response.

like image 23
Servy Avatar answered Sep 30 '22 04:09

Servy