Hello, I wanted to try out such a tasty C # as async/await and wrote a test program:

 class MySynchronizationContext : SynchronizationContext { public override void Post(SendOrPostCallback d, object state) { Console.WriteLine("Post"); base.Post(d, state); } public override void Send(SendOrPostCallback d, object state) { Console.WriteLine("Send"); base.Send(d, state); } } public class Program { public static void Main(string[] args) { SynchronizationContext.SetSynchronizationContext(new MySynchronizationContext()); var browsers = GetBrowsers(); Console.WriteLine("start"); Console.WriteLine(browsers.Result); } static async Task<string> GetBrowsers() { var res = string.Empty; var bd = await GetFromFS(); res += bd; // <-тут могла быть операция с UI var net = await GetFromNet(); res += " and " + net; // <-тут могла быть операция с UI var cpu = await Task.Run<string>(() => { Thread.Sleep(200); return "edge"; }); res += " and " + cpu; // <-тут могла быть операция с UI return res; } static async Task<string> GetFromFS() { using (var sr = new StreamReader(@"C:\bd.txt")) { var res = await sr.ReadToEndAsync(); // тут могла быть операция с UI return res + " and firefox"; } } static async Task<string> GetFromNet() { await Task.Delay(200); // тут могла быть операция с UI return "chrome"; } } 

I expected that after each await'а my context will be restored (after all, if this application is with the UI, then you need to access the controls), which means that after each await'а , “Post” will be displayed in the console, but what was My surprise when I got only 2 times. Hence the question:

  1. why the context is not always attached?

Now to async/await : as I understand it, starting with var browsers = GetBrowsers(); the call will be made on the stack down to the first task (in this case, sr.ReadToEndAsync(); or maybe lower), and the control will immediately go back to the Main() method, without waiting for the task to complete; the rest of the async methods will be executed after the execution of this task, and so on, from which the following question arises:

  1. Who is waiting for the very first task (then the next one, when will the queue come after await'а ), change its state to RanToCompletion ? I think it would be foolish to think that a thread from the thread pool is allocated for waiting, right?

The third question is at the junction of task and thread pool. The continuation after await , as I understand it, is performed in the thread pool (the context is passed to it), but ...

  1. What puts a part of the code after await into the thread pool and when (when did the task complete or when it was created?)?

Thanks in advance for clarifying the situation

  • However, try the same code in the goove application. Will something change? - Alexander Petrov
  • @AlexanderPetrov yes, it will change, but this raises even more questions than answers, since async \ await deals with context switching. Roughly speaking, if I know the answer to my first question - most likely I will know why everything is a bit different in the UI, so the question remains open - Qutrix
  • Perhaps this article will clarify a bit how asynchrony is implemented if we drop all the async \ await syntactic sugar and look at it all in a clean TPL. bit.ly/2G269sp - Liashenko V

2 answers 2

  1. The fact is that the execution of the Post() method in your context you delegate to the base method. And the base method puts the delegate to execute in the thread pool . In the case of the UI, this does not happen, because, for example, the WinForms context completely overrides methods. Insert the context and current stream logging into your code:

     class MySynchronizationContext : SynchronizationContext { public override void Post(SendOrPostCallback d, object state) { Console.WriteLine($"Post, thread id = {Thread.CurrentThread.ManagedThreadId}"); base.Post(d, state); } public override void Send(SendOrPostCallback d, object state) { Console.WriteLine($"Send, thread id: {Thread.CurrentThread.ManagedThreadId}"); base.Send(d, state); } public override string ToString() { return "My"; } } public class Program { public static void Main(string[] args) { SynchronizationContext.SetSynchronizationContext(new MySynchronizationContext()); var browsers = GetBrowsers(); Console.WriteLine("start"); Console.WriteLine(browsers.Result); } static async Task<string> GetBrowsers() { LogCurrentContext("GetBrowsers prologue"); var res = string.Empty; var bd = await GetFromFS(); LogCurrentContext("GetBrowsers after GetFromFS"); res += bd; // <-тут могла быть операция с UI var net = await GetFromNet(); LogCurrentContext("GetBrowsers after GetFromNet"); res += " and " + net; // <-тут могла быть операция с UI var cpu = await Task.Run<string>(() => { Thread.Sleep(200); return "edge"; }); LogCurrentContext("GetBrowsers after Task.Run"); res += " and " + cpu; // <-тут могла быть операция с UI return res; } private static void LogCurrentContext(string message) { Console.WriteLine($"{message}: {(SynchronizationContext.Current?.ToString() ?? "default")} context, thread id = {Thread.CurrentThread.ManagedThreadId}"); } static async Task<string> GetFromFS() { LogCurrentContext("GetFromFS prologue"); using (var sr = new StreamReader(@"D:\GetEventsMarkets.sql")) { var res = await sr.ReadToEndAsync(); LogCurrentContext("GetFromFS after ReadToEndAsync"); // тут могла быть операция с UI return res + " and firefox"; } } static async Task<string> GetFromNet() { LogCurrentContext("GetFromNet prologue"); await Task.Delay(200); // тут могла быть операция с UI LogCurrentContext("GetFromNet after Task.Delay"); return "chrome"; } } 

    Most often, I got a similar result:

     GetBrowsers prologue: My context, thread id = 1 GetFromFS prologue: My context, thread id = 1 Post, thread id = 1 GetFromFS after ReadToEndAsync: default context, thread id = 3 start Post, thread id = 3 GetBrowsers after GetFromFS: default context, thread id = 3 GetFromNet prologue: default context, thread id = 3 GetFromNet after Task.Delay: default context, thread id = 4 GetBrowsers after GetFromNet: default context, thread id = 4 GetBrowsers after Task.Run: default context, thread id = 4 

    Here we are interested in two things.

    • In a custom context, only the prologues of the GetBrowsers() and GetFromFS() methods are GetBrowsers() . After the await sr.ReadToEndAsync() string is executed, the continuation of the GetFromFS() method is GetFromFS() to the custom context. This is the first call to Post() . Next, the GetFromFS() method is completed and the continuation of the GetBrowsers() method is again fastened to the custom context. This is the second call to Post() . However, since the custom context simply runs the code in the thread pool, these continuations already work in the context of the thread pool . That is why we no longer see calls to custom context.
    • The GetFromFS() method started to run in the stream with id = 1. However, the continuation itself was performed in the stream with id = 3 for the reason described above. The continuation after the call GetFromFS() was called in the same thread ( Post, thread id = 3 ).
  2. The very first task, like any other one, is waited by a special IO thread (the so-called IO completion port, IOCP). But such streams are waiting for a very large number of completions, incl. and Tasks, therefore, to speak about "one task - one waiting stream" is not necessary. Dive deep into and read more in the article on Habré .

  3. Continuation of the async method is performed in the captured context. If there is no such context (for example, when calling with ConfigureAwait(false) ) - the continuation is performed in the context of the thread pool. The continuation call in the appropriate context is performed by the compiler - it generates the corresponding code with the Post() call. The compiler parses the async method into its components (prologue + continuations) and generates a state machine with transitions from them. I highly recommend watching this talk (or at least the slides ), as well as familiarize yourself with this answer . After this, phrases like "async / await do context switching" should disappear from your daily life.

As a summary: you get embarrassing results because your context implementation is, strictly speaking, incorrect. In essence, it is similar to the thread pool context that is used for console applications and in which all continuations are executed if no other context has been detected.

  • Comments are not intended for extended discussion; conversation moved to chat . - PashaPash
  1. why the context is not always attached?

The problem with your context is that it does not know how to restore itself. If you write your context - then you yourself must take care that all the continuations run in it.

  1. who is waiting for the very first task

If the asynchrony is correct, then the “very first” Task , like all subsequent ones, is created using the TaskCompletionSource mechanism.

For example, like this (I’m giving the code for example only, in reality, you still need to handle exceptions):

 Task<int> VeryFirstTask() { var tcs = new TaskCompletionSource<int>(); Action handler = null; handler = () => { tcs.SetResult(42); SomeEvent -= handler; }; SomeEvent += handler; return tcs.Task; } 

Events are not necessarily used - but the idea is the same. Somewhere there is a TaskCompletionSource , in which SetResult / SetException / SetCanceled TaskCompletionSource at the right moment.

  1. What puts a part of the code after await into the thread pool and when (when did the task complete or when it was created?)?

Directly puts it there synchronization context.

Indirectly, a data structure such as TaskAwaiter is involved in this (it stores the captured synchronization context).