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:
And in IL:
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).
Strange, I couldn’t explain, I executed and it didn’t happen.
– Maniero
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.
– DanOver
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.
– Maniero
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.
– DanOver
Tasks run on threads in the threadpool. async/await generates a Task.
– user178974