9

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!

morgwai
  • 2,513
  • 4
  • 25
  • 31
  • `doSthAsync` **could** have resumed onto the same thread it used initially - if it weren't for the fact that you've tied that thread up inside of a blocking `Thread.Sleep()` call. It cannot steal a thread that's blocked doing something else. Plus, in this case, the context means that it is *possible* to use other thread pool threads here and they're considered equal. Don't immediately dismiss the article and leap to the opposite conclusions. – Damien_The_Unbeliever Dec 12 '16 at 13:39
  • @Damien_The_Unbeliever Thread.sleep was used to "simulate" any other processing that is longer than the asynchronous operation (here simulated by Task.Delay). removing Thread.sleep still makes "async 2" to be performed by a separate thread. also I don't want it to "steal" this thread, I want it to wait with passing control back to async method until the thread is done with it's work and calls Task.Wait() or "await something();". I will rephrase the question to make it clear. – morgwai Dec 12 '16 at 13:44
  • 1
    Before you go too far and leap to too many conclusions, you should also be aware that console applications aren't going to be a good context for understanding other types of application. To understand that, you need to read up on [Synchronization contexts](https://blogs.msdn.microsoft.com/pfxteam/2012/01/20/await-synchronizationcontext-and-console-apps/) – Damien_The_Unbeliever Dec 12 '16 at 13:48
  • @Damien_The_Unbeliever console app was just to illustrate it simply: as I wrote I'm asking this question in context of ASP.NET Core and "async" entity framework DB calls: any hints in "web context" would be appreciated :) – morgwai Dec 12 '16 at 14:01
  • The article is wrong to state that "the whole point" of `await` is to run the continuations on the same thread. The whole point is to make it easy to write code using a continuation passing style where the code runs asynchronously even though it's written much more like traditional synchronous code. The ability to have those continuations all be run by a particular thread is one feature of that system, and it is there, but there are sensible reasons to use `await` even when you can't use that one feature. – Servy Dec 12 '16 at 14:40

3 Answers3

10

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.

Servy
  • 202,030
  • 26
  • 332
  • 449
  • Same like there http://stackoverflow.com/questions/19543240/what-thread-runs-the-code-after-the-await-keyword – J. Doe Dec 12 '16 at 14:41
  • @Servy, what is "message" in this context? awaitable "called" with await keyword or sth else? I'm also not sure what "there is never more than one operation running at a time" means: does it mean that I can have at most 1 outstanding DB call? I guess not as that would be quite lame... Thx! – morgwai Dec 12 '16 at 15:39
  • @morgwai in this context a "message" is an operation for the message loop to process. You can't ever be performing more than one operation *in the context of the current request*. If you're performing operations outside of that context, then they wouldn't count towards that. – Servy Dec 12 '16 at 15:52
  • Isn't ASP.NET Core fully async and free threaded (no synchronization context)? – Paulo Morgado Dec 12 '16 at 15:53
  • @PauloMorgado That it's specifically set up to be asynchronous is exactly why it needs a synchronization context. – Servy Dec 12 '16 at 15:54
  • @Servy, I just tried this `public HomeController(ILoggerFactory loggerFactory) { loggerFactory.CreateLogger("X").LogInformation(System.Threading.SynchronizationContext.Current?.ToString() ?? ""); }` and it prints ****- – Paulo Morgado Dec 12 '16 at 16:11
  • @Servy, first please confirm if I understand the terminology correctly: as I understand an async call called with await keyword adds a "message" to a "message loop", right? So in such case statement "You can't ever be performing more than one operation in the context of the current request" would mean that it is impossible for example to trigger an async download of 2 separate web pages in parallel during processing of a single http request using only async/await mechanism (without spawning a thread explicitly), right? – morgwai Dec 12 '16 at 16:35
  • @morgwai No, it just means that those operations aren't going to happen in the context in which they're called. Since they're IO, they don't run in any thread at all. – Servy Dec 12 '16 at 16:42
6

By default when there is not SynchronizationContext or when ConfigureAwait(false) is called on awaited task, its continuation will run on CLR maintained thread-pool that can be accessed using static methods from ThreadPool class
in .net core methods from ThreadPool to control its size (setMaxThreads and setMinThreads) are not implemented yet:
https://github.com/dotnet/cli/issues/889
https://github.com/dotnet/corefx/issues/5920
it is possible however to set its size statically in project.json file as explained here:
https://github.com/dotnet/cli/blob/rel/1.0.0/Documentation/specs/runtime-configuration-file.md#runtimeoptions-section-runtimeconfigjson

kestrel has its own pool for async processing of requests with libuv: it's controlled by static ThreadCount property of KestrelServerOptions class.

related articles:
http://blog.stephencleary.com/2012/02/async-and-await.html
http://blog.stephencleary.com/2012/07/dont-block-on-async-code.html
https://blogs.msdn.microsoft.com/pfxteam/2012/01/20/await-synchronizationcontext-and-console-apps/
https://msdn.microsoft.com/en-us/magazine/gg598924.aspx

thx to @Servy and @Damien_The_Unbeliever for the hints that allowed me to find it

morgwai
  • 2,513
  • 4
  • 25
  • 31
3

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?

AmitE
  • 884
  • 1
  • 9
  • 13