Async/await generates a new thread?

Asked

Viewed 247 times

5

When reading about async/await methods, it is said that using these keywords alone will not cause your method to run in a new thread. However, I get the output below by running my code. A thread was generated to run the code that comes just below the await. I would like to understand why this occurs.

19:23:38.277 Thread #692 Program Start 19:23:38.293 Thread #692 Experiment code is synchronous before await 19:23:38.293 Thread #692 Experiment code is asynchronous after first await 19:23:38.293 Thread #13 Task I created (running on pool thread)
19:23:38.293 Thread #512 I didn’t ask for this line to run on a Different thread. 19:23:38.293 Thread #692 Method execution home 19:23:38.293 Thread #13 I didn’t ask for this line to be executed on a different thread (2).

using System;
using System.Threading;
using System.Threading.Tasks;

public class Program
{
    public static void Main()
    {
        Program p = new Program();
        p.Run();
    }

    private void Print(string txt)
    {
        string dateStr = DateTime.Now.ToString("HH:mm:ss.fff");
        Console.WriteLine("{0} Thread #{1}\t{2}", dateStr, Thread.CurrentThread.ManagedThreadId, txt);
    }

    private void Run()
    {
        Print("Program Start");
        Experiment();
        Print("Execução do metodo principal");  
        Console.ReadLine();
    }

    private async Task Experiment()
    {
        Print("Experiment code is synchronous before await");
        await SomethingElse();
        Print(" Não pedi para essa linha ser executada em thread diferente (2).");
    }

    private async Task SomethingElse()
    {
        Print("Experiment code is asynchronous after first await");
        string dateStr = DateTime.Now.ToString("HH:mm:ss.fff");
        await Task.Run(() =>  Print("Task que eu criei (rodando em thread do pool)"));
        Print(" Não pedi para essa linha ser executada em uma thread diferente.");
            }
}
  • Strange, I couldn’t explain, I executed and it didn’t happen.

  • I ran on Linqpad 6 now here and the result has changed a bit, but the doubt remains, although I already know what happens. When I run the Task.Run method, it takes a thread from the pool to run past code. At this point the main thread returns execution of my Run method. In my asynchronous method, there are still code snippets to be executed after await. Apparently a pool thread is again asked to run them.

  • 1

    Yes, but I don’t think it should happen (even if it does happen) and I find it strange that it happens only in some cases.

  • Before this experience, if I had to "kick" an opinion, I would say that the main thread would give up time to perform what remained after the await in asynchronous methods, but now I see that it is not quite so.

  • 1

    Tasks run on threads in the threadpool. async/await generates a Task.

1 answer

1

The short answer is: The use of await generates a state machine that contains Awaitunsafeoncomplete, this method does not maintain the execution context.

EDIT: It is worth mentioning that the use of Awaitunsafeoncomplete is strategic, because it would hardly occur in a real scenario. In this your experiment occurred, because the execution is almost instantaneous. Try to use this way:

await Task.Run(() => PrintLongo("Task que eu criei (rodando em thread do pool)"));


private void PrintLongo(string txt)
{
    Task.Delay(200);
    string dateStr = DateTime.Now.ToString("HH:mm:ss.fff");
    Console.WriteLine("{0} Thread #{1}\t{2}", dateStr, Thread.CurrentThread.ManagedThreadId, txt);
}

To better understand, we have to analyze the generated code.

Given the code below:

await Task.Run(() =>  Print("Task que eu criei (rodando em thread do pool)"));

We can see the state machine that was generated from your example:

Máquina de estados gerada pelo uso do await

And in IL:

Código em Intermediate Language

Watching the lines:

IL_003e: call instance bool [System.Runtime]System.Runtime.CompilerServices.TaskAwaiter::get_IsCompleted()
IL_0043: brtrue.s IL_0085

We see that when running get_IsCompleted and if true it advances to:

    IL_0085: ldloca.s 1
    IL_0087: call instance void [System.Runtime]System.Runtime.CompilerServices.TaskAwaiter::GetResult()
    IL_008c: nop
    IL_008d: ldarg.0
    IL_008e: ldfld class Program Program/'<SomethingElse>d__4'::'<>4__this'
    IL_0093: ldstr " Não pedi para essa linha ser executada em uma thread diferente."
    IL_0098: call instance void Program::Print(string)
    IL_009d: nop
    IL_009e: leave.s IL_00b8

The focus here is to notice the execution context, which keeps thread information in a logical flow of control. We have methods that already do this automatically, such as Task.Run. The method captures call chaining and stores it with the instance. When the task associate executes a particular delegate, it executes it using the stored context. However, not all methods in BCL will automatically capture and restore the execution context. As we can see, according to the use in your experiment, the generated code provides us with a AsyncMethodBuilder.AwaitUnsafeOnComplete, This method is marked as "unsafe".

Following your experiment, I switch to:

Task.Run(() => Print("Task que eu criei (rodando em thread do pool)")).GetAwaiter().GetResult();

And then, with the use of Getresult() we no longer use Awaitunsafeoncomplete and the context is maintained.

IL_0007: nop
IL_0008: ldarg.0
IL_0009: ldfld class Program Program/'<SomethingElse>d__4'::'<>4__this'
IL_000e: ldstr "Experiment code is asynchronous after first await"
IL_0013: call instance void Program::Print(string)
IL_0018: nop
IL_0019: ldarg.0
IL_001a: ldfld class Program Program/'<SomethingElse>d__4'::'<>4__this'
IL_001f: ldftn instance void Program::'<SomethingElse>b__4_0'()
IL_0025: newobj instance void [System.Runtime]System.Action::.ctor(object, native int)
IL_002a: call class [System.Runtime]System.Threading.Tasks.Task [System.Runtime]System.Threading.Tasks.Task::Run(class [System.Runtime]System.Action)
IL_002f: callvirt instance valuetype [System.Runtime]System.Runtime.CompilerServices.TaskAwaiter [System.Runtime]System.Threading.Tasks.Task::GetAwaiter()
IL_0034: stloc.1
IL_0035: ldloca.s 1
IL_0037: call instance void [System.Runtime]System.Runtime.CompilerServices.TaskAwaiter::GetResult()
IL_003c: nop
IL_003d: ldarg.0
IL_003e: ldfld class Program Program/'<SomethingElse>d__4'::'<>4__this'
IL_0043: ldstr " Não pedi para essa linha ser executada em uma thread diferente."
IL_0048: call instance void Program::Print(string)
IL_004d: nop
IL_004e: leave.s IL_0068

Running with Getresult(), we have output:

01:37:18.169 Thread #1  Program Start
01:37:18.232 Thread #1  Experiment code is synchronous before await
01:37:18.233 Thread #1  Experiment code is asynchronous after first await
01:37:18.300 Thread #4  Task que eu criei (rodando em thread do pool)
01:37:18.302 Thread #1   Não pedi para essa linha ser executada em uma thread diferente.
01:37:18.302 Thread #1   Não pedi para essa linha ser executada em thread diferente (2).

inserir a descrição da imagem aqui

Browser other questions tagged

You are not signed in. Login or sign up in order to post.