<<

Throughout the entire process, a key takeaway is that no thread is dedicated to running the task.

HTTPS://DOCS.MICROSOFT.COM/EN - US/DOTNET/STANDARD /ASYNC - IN- DEPTH

05.05.2021 INTERNALS OF ASYNC - ADAM FURMANEK 1 Internals of Async [email protected] HTTP://BLOG.ADAMFURMANEK.PL FURMANEKADAM

05.05.2021 INTERNALS OF ASYNC - ADAM FURMANEK 2 About me Software Engineer, Blogger, Book Writer, Public Speaker. Author of Applied Integer Linear Programming and .NET Internals Cookbook. http://blog.adamfurmanek.pl [email protected] furmanekadam

05.05.2021 INTERNALS OF ASYNC - ADAM FURMANEK 3 Agenda Primitves under the hood. Task detail. SynchronizationContext internals. State machine. Waiting for async void and handling exceptions

05.05.2021 INTERNALS OF ASYNC - ADAM FURMANEK 4 Primitives under the hood

05.05.2021 INTERNALS OF ASYNC - ADAM FURMANEK 5 Native thread Two types: foreground and background (don’t stop application from terminating). Consists of Thread Kernel Object, two stacks (user mode and kernel mode) and Thread Environment Block (TEB). User mode stack by default has 1 MB, kernel mode has 12/24 KB. Has impersonation info, security context, Thread Local Storage (TLS). Windows schedules threads, not processes! How many threads does the notepad.exe have?

05.05.2021 INTERNALS OF ASYNC - ADAM FURMANEK 6 How many threads does a notepad have?

05.05.2021 INTERNALS OF ASYNC - ADAM FURMANEK 7 Managed thread Has ID independent of native thread ID. Can have name. Can be suspended but this should not be done! Can be aborted by Thread.Abort but this doesn’t guarantee anything. Precommits stack when created. Unhandled exception kills the application in most cases. In .NET 1 it was different: ◦ Exception in other thread was printed to the console and the thread was terminated. ◦ Exception on the finalizer was printed to the console and finalizer was still working. ◦ Exception on the main thread resulted in application termination.

05.05.2021 INTERNALS OF ASYNC - ADAM FURMANEK 8 ThreadPool Different from Win32 thread pool. Used by tasks, asynchronous timers, wait handles and ThreadPool.QueueUserWorkItem. Static class – you cannot just create your own thread pool. Threads work in background, do not clear the TLS, have default stack size and default priority. One pool per process, its size depends on the size of the virtual address space. Threads are created and destroyed as needed using hill climbing algorithm. Two types of threads: for ordinary callbacks and for I/O operations. Thrown exception is held until awaiting and then propagated if possible (thrown out of band for async void). In .NET 1 it was different - exception on a thread pool was printed to the console and the thread was returned to the pool.

http://aviadezra.blogspot.com/2009/06/net-clr-thread-pool-work.html

05.05.2021 INTERNALS OF ASYNC - ADAM FURMANEK 9 ThreadPool implementation public static class SimpleThreadPool { private static BlockingCollection _work = new BlockingCollection(); static SimpleThreadPool() { for (int i = 0; i < Environment.ProcessorCount; i++) { new Thread(() => { foreach (var action in _work.GetConsumingEnumerable()) { action(); } }) { IsBackground = true }.Start(); } } public static void QueueWorkItem(Action workItem) { _work.Add(workItem); } }

05.05.2021 INTERNALS OF ASYNC - ADAM FURMANEK 10 Asynchronous Programming Model (APM) BeginOperation returns an object implementing var fs = new FileStream(@"C:\file.txt"); IAsyncResult. ◦ Triggers the asynchronous calculations on different byte[] data = new byte[100]; thread. fs.BeginRead(data, 0, data.Length, ◦ Can also accept a callback to be called when the operation is finished. (IAsyncResult ar) => IAsyncResult: { ◦ Has some AsyncState. int bytesRead = fs.EndRead(ar); ◦ Contains WaitHandle which we can use to block the application. fs.Close(); ◦ Has flag indicating whether the operation is completed. }, null EndOperation accepts IAsyncResult as a parameter ); and returns the same as synchronous counterpart. ◦ Throws all exceptions if needed. ◦ If the operation hasn’t finished, blocks the thread.

05.05.2021 INTERNALS OF ASYNC - ADAM FURMANEK 11 Event-based Asynchronous Pattern (EAP) MethodNameAsync. backgroundWorker.DoWork += backgroundWorker_DoWork; ◦ Triggers the operation on a separate thread. private void backgroundWorker_DoWork(object sender, DoWorkEventArgs e) MethodNameCompleted. ◦ Event fired when the operation finishes. { ◦ Passes parameter AsyncCompletedEventArgs. // ... AsyncCompletedEventArgs: } ◦ Contains flag if the job was cancelled. private void backgroundWorker_RunWorkerCompleted(object sender, ◦ Contains all the errors. RunWorkerCompletedEventArgs e) ◦ Has some UserState. { Can be canceled. // ... Can be used easily with BackgroundWorker. }

05.05.2021 INTERNALS OF ASYNC - ADAM FURMANEK 12 Task-based Asynchronous Pattern (TAP) Task.Run accepting delegate triggers the job: ◦ Equivalent to Task.Factory.StartNew(job, CancellationToken.None, TaskCreationOptions.DenyChildAttach, TaskScheduler.Default); ◦ Unwraps the result if needed (so we get Task instead of Task>). Task can be created manually via constructor and schedulled using Start method. Can be joined by using ContinueWith. Exceptions are caught and propagated on continuation. Can be used with TaskCompletionSource. Can be cancelled with CancellationToken. Can report progress using IProgress.

05.05.2021 INTERNALS OF ASYNC - ADAM FURMANEK 13 Parallel Language Integrated Queries (PLINQ) Created when AsParallel called on IEnumerable. Can be reverted by AsSequential. Operations defined in ParallelEnumerable class. Can be ordered by calling AsOrdered. Task merging can be configured by specifying ParallelMergeOptions. Maximum number of concurrent tasks can be controlled using WithDegreeOfParallelism. Parallelism is not mandatory! Can be forced with ParallelExcecutionMode. Each AsParallel call reshuffles the tasks.

05.05.2021 INTERNALS OF ASYNC - ADAM FURMANEK 14 async and await await can be executed on anything awaitable – not necessarily a Task! ◦ Task.Yield returns YieldAwaitable Duck typing - awaitable type must be able to return GetAwaiter() with the following: ◦ Implements INotifyCompletion interface ◦ bool IsCompleted { get; } ◦ void OnCompleted(Action continuation); ◦ TResult GetResult(); // or void async means nothing — it only instructs the compiler to create a state machine. We can make any type awaitable using extension methods! Very similar to foreach.

05.05.2021 INTERNALS OF ASYNC - ADAM FURMANEK 15 Awaiting on integer

05.05.2021 INTERNALS OF ASYNC - ADAM FURMANEK 16 Asynchronous code does not block the operating system level thread.

05.05.2021 INTERNALS OF ASYNC - ADAM FURMANEK 17 async in C# async in C# is implemented as: ◦ compiler level transformation with ◦service locator for promise orchestration and ◦statically bound promise factories

05.05.2021 INTERNALS OF ASYNC - ADAM FURMANEK 18 Task details STATICALLY BOUND PROMISE FACTORIES

05.05.2021 INTERNALS OF ASYNC - ADAM FURMANEK 19 Two types of tasks

DELEGATE TASK – CPU-BOUND PROMISE TASK – I/O-BOUND

Has some code to run. Signals completion of something. Mostly in TPL world. Mostly in async world. Created by TaskFactory or by constructor. Task.FromResult ◦ Creates completed Task with result. Used in PLINQ. Task.Delay Can be scheduled and executed. ◦ Equivalent of Thread.Sleep. Task.Yield ◦ Returns YieldAwaitable. ◦ Schedules continuation immediately

05.05.2021 INTERNALS OF ASYNC - ADAM FURMANEK 20 Task state

https://blog.stephencleary.com/2014/06/a-tour-of-task-part-3-status.html

05.05.2021 INTERNALS OF ASYNC - ADAM FURMANEK 21 Task creation

CONSTRUCTOR FACTORY Do not use! Task.Run() Task.Factory.StartNew() Creates only delegate Task. PLINQ Created Task is not scheduled so will not start running unless asked to. Created Task can be started by calling Start method (and optionally providing a scheduler).

05.05.2021 INTERNALS OF ASYNC - ADAM FURMANEK 22 Task.ScheduleAndStart

05.05.2021 INTERNALS OF ASYNC - ADAM FURMANEK 23 TaskScheduler Schedules tasks on the threads – it makes sure that the work of a task is eventually executed. For TPL and PLINQ is based on the thread pool. Supports work-stealing, thread injection/retirement and fairness. Two types of queues: ◦ Global – for top level tasks ◦ Local – for nested/child tasks, accessed in LIFO order Long running tasks are handled separately, do not go via global/local queue. We can implement our own schedulers.

05.05.2021 INTERNALS OF ASYNC - ADAM FURMANEK 24 TaskScheduler implementation public class MyTaskScheduler : TaskScheduler { private readonly MyContext context; public BlockingCollection tasks = new BlockingCollection();

protected override IEnumerable GetScheduledTasks() { return tasks; }

protected override void QueueTask(Task task) { tasks.Add(task); }

protected override bool TryExecuteTaskInline(Task task, bool taskWasPreviouslyQueued) { return TryExecuteTask(task); } }

05.05.2021 INTERNALS OF ASYNC - ADAM FURMANEK 25 Task.ContinueWith Creates a continuation that executes asynchronously when the target task complets ◦ Can specify CancellationToken ◦ Can specify TaskScheduler ◦ Can specify TaskContinuationOptions Options: ◦ OnlyOnCompletion, OnlyOnCanceled, OnlyOnFaulted, NotOnCanceled, NotOnFaulted, NotOnCompletion – to choose when it is supposed to run ◦ AttachedToParent – to create hierarchy of tasks ◦ ExecuteSynchronously, RunContinuationAsynchronously – to choose the thread running it ◦ HideScheduler – to run using the default scheduler instead of the current one ◦ LongRunning – more or less to run on dedicated thread ◦ Prefer fairness – to run in order

05.05.2021 INTERNALS OF ASYNC - ADAM FURMANEK 26 Task.ContinueWith

05.05.2021 INTERNALS OF ASYNC - ADAM FURMANEK 27 Task.Complete

05.05.2021 INTERNALS OF ASYNC - ADAM FURMANEK 28 Task.TrySetResult

05.05.2021 INTERNALS OF ASYNC - ADAM FURMANEK 29 Task.FinishContinuations

05.05.2021 INTERNALS OF ASYNC - ADAM FURMANEK 30 StandardTaskContinuation.Run

05.05.2021 INTERNALS OF ASYNC - ADAM FURMANEK 31 Disposing a Task Task may allocated WaitHandle which implements IDisposable. Disposing a Task in .NET 4 was making it unusable — we couldn’t even schedule continuation. In .NET 4.5 this was changed, Task is still usable, only WaitHandle is not. WaitHandle was created when Task.WaitAny or Task.WaitAll was called, this is no longer true. Starting in .NET 4.5 WaitHandle is allocated only when it is explicitly accessed. Summary: ◦ .NET 4 — don’t dispose unless you have to. Do so only if you are sure that the Task will never be used again. ◦ .NET 4.5 — it shouldn’t make a difference so probably don’t bother.

05.05.2021 INTERNALS OF ASYNC - ADAM FURMANEK 32 Task.Status Task.Id

State IsCompleted IsCanceled IsFaulted Generated on demand. RanToCompletion True False False Can be reused — you can generate collision! Canceled True True False Independent from TaskScheduler.Id. Faulted True False True 0 is not a valid identifier. Other False False False

05.05.2021 INTERNALS OF ASYNC - ADAM FURMANEK 33 ValueTask Task is a class so it is allocated on the heap and needs to be collected by GC. To avoid explicit allocation we can use ValueTask which is a struct and is allocated on the stack. The trick is in the second constructor parameter — the token. public ValueTask(IValueTaskSource source, short token); See https://github.com/kkokosa/PooledValueTaskSource Conceptually it was used in Midori — .NET-based operating system implemented by Microsoft Research. „It still kills me that I can’t go back in time and make .NET’s task a struct” — Joe Duffy in http://joeduffyblog.com/2015/11/19/asynchronous-everything/

05.05.2021 INTERNALS OF ASYNC - ADAM FURMANEK 34 SynchronizationContext internals SERVICE LOCATOR FOR PROMISE ORCHESTRATION

05.05.2021 INTERNALS OF ASYNC - ADAM FURMANEK 35 ISynchronizeInvoke – life before SynchronizationContext Provides a way to synchronously or asynchronously execute a delegate: ◦ InvokeRequired – checks if invoking is requred, effectively if we are running on the same thread ◦ Invoke – synchronous invocation ◦ BeginInvoke, EndInvoke – asynchronous invocation It ties communication and threads. If we don’t need specific thread – as in ASP.NET – we should not use ISynchronizeInvoke. This is how SynchronizationContext emerged.

05.05.2021 INTERNALS OF ASYNC - ADAM FURMANEK 36 ExecutionContext and other Bag holding logical context of the execution. Contains SynchronizationContext, LogicalCallContext, SecurityContext, HostExecutionContext, CallContext etc. Does not need to rely on Thread Local Storage (TLS) and is passed correctly through asynchronous points — will follow to the other thread. Before .NET 4.5 LogicalCallContext was performing shadow copies and couldn’t be used between asynchronous points of invocation. Starting in .NET 4.6 there is an AsyncLocal class working as TLS variables for tasks. Methods with Unsafe* do not propagate the context — for instance ThreadPool.UnsafeQueueUserWorkItem.

05.05.2021 INTERNALS OF ASYNC - ADAM FURMANEK 37 AsyncLocal

05.05.2021 INTERNALS OF ASYNC - ADAM FURMANEK 38 SynchronizationContext The SynchronizationContext class is a base class that provides a free-threaded context with no synchronization: ◦ OperationStarted and OperationCompleted – handles notifications ◦ Send – synchronous message ◦ Post – asynchronous message ◦ Current – gets synchronization context for the thread The purpose of the synchronization model implemented by this class is to allow the internal asynchronous/synchronous operations of the to behave properly with different synchronization models

05.05.2021 INTERNALS OF ASYNC - ADAM FURMANEK 39 SynchronizationContext When awaiting the awaitable type the current context is captured. Later, the rest of the method is posted on the context. We can use ConfigureAwait(false) to avoid capturing the context. Rule of thumb — always use it unless you are sure that you need a context.

05.05.2021 INTERNALS OF ASYNC - ADAM FURMANEK 40 SynchronizationContext Synchronization context is a global variable. If SynchronizationContext.Current is not null then this context is captured: ◦ For UI thread it is UI context – WindowsFormsSynchronizationContext, DispatcherSynchronizationContext, WinRTSynchronizationContext, WinRTCoreDispatcherBasedSynchronizationContext ◦ Implemented via event loop, for instance. ◦ For ASP.NET request it is ASP.NET context — AspNetSynchronizationContext ◦ This can be different thread than original one, but still the request context is the same. Otherwise it is current TaskScheduler: ◦ TaskScheduler.Default is the thread pool context. ◦ ASP.NET Core doesn’t have spearate context – no risk of deadlock, no need to use ConfigureAwait(false) Each method can have its own context.

05.05.2021 INTERNALS OF ASYNC - ADAM FURMANEK 41 SynchronizationContext

Specific thread Delegates Delegates Send is Post is executing the code executed serially executed in order synchronous asynchronous Default (Thread No – any thread in No No Yes Yes Pool based) the thread pool ASP.NET No – any thread in Yes No Yes No the thread pool WinForms, WPF, Yes – UI thread Yes Yes Only if called on Yes WinRT, Xamarin, the UI thread Blazor ASP.NET Core No – any thread in No No Yes Yes the thread pool In ASP.NET only one continuation can be executed at a time for given request - no concurrency. In ASP.NET Core multiple continuations can run concurrently – we have concurrency and parallelism.

05.05.2021 INTERNALS OF ASYNC - ADAM FURMANEK 42 State machine COROUTINE COMPILER LEVEL TRANSFORMATION

05.05.2021 INTERNALS OF ASYNC - ADAM FURMANEK 43 State machine 1 – before compilation OurAsyncMethod has four parts ◦ First part will run synchronously as the Task.FromResult is already resolved ◦ Second part will eventually block because of the delay ◦ Third part will block and explicitly create continuation ◦ Fourth part with just throw exception

05.05.2021 INTERNALS OF ASYNC - ADAM FURMANEK 44 State machine 2 – method after compilation async is no longer there — it is only on C# level. DebuggerStepThroughAttribute tells the debugger to step through (ignore) the method. Program.d__1 d__ type created to encapsulate state machine pieces. State is initialized to -1 meaning „ready to do some work”. AsyncTaskMethodBuilder is a .NET class capable of executing the state machine. Effectively we prepare the machine, start it and return the Task object with the result.

05.05.2021 INTERNALS OF ASYNC - ADAM FURMANEK 46 State machine 3 – fields Program.d__1 d__ type created to encapsulate state machine pieces. d__.<>1__state variable maintains the state ◦ Initially it is set to -1 meaning „not started” ◦ -2 means „done” ◦ Non-negative states indicate different pieces of the state machine Three different awaiters as we have await three times in the original method.

05.05.2021 INTERNALS OF ASYNC - ADAM FURMANEK 47 State machine 4 – Start method

05.05.2021 INTERNALS OF ASYNC - ADAM FURMANEK 48 State machine 5 – exception handling We capture the state to local variable. We handle all exceptions and terminate the machine if needed.

05.05.2021 INTERNALS OF ASYNC - ADAM FURMANEK 49 State machine 6 – states Awaiter at the beginning for different result types. Four different branches as we have await three times generating four blocks.

05.05.2021 INTERNALS OF ASYNC - ADAM FURMANEK 50 State machine 7 – num = -1, state = -1 await Task.FromResult(false); It starts in default. We print to the console and get awaiter for the result. If it is completed (as this is the case now) ◦ We call GetResult which returns the value immediately ◦ We end in line 72 ◦ num = -1, state = -1

05.05.2021 INTERNALS OF ASYNC - ADAM FURMANEK 51 State machine 8 – num = -1, state = -1 await Task.Delay(200); It starts in line 73. We print to the console and get the awaiter. If it is not completed ◦ We change the state (so we know where to come back) ◦ We call AwaitUnsafeOnCompleted (see in a bit) ◦ And then we return Later we continue in case 1 ◦ We jump to the label IL_FA and end in line 84 ◦ num = 1, state = -1

05.05.2021 INTERNALS OF ASYNC - ADAM FURMANEK 52 AsyncTaskMethodBuilder.AwaitUnsafeOnCompleted

05.05.2021 INTERNALS OF ASYNC - ADAM FURMANEK 53 AwaitUnsafeOnCompleted — GetCompletionAction

05.05.2021 INTERNALS OF ASYNC - ADAM FURMANEK 54 MoveNextRunner

05.05.2021 INTERNALS OF ASYNC - ADAM FURMANEK 55 TaskAwaiter.AwaitUnsafeOnCompleted

05.05.2021 INTERNALS OF ASYNC - ADAM FURMANEK 56 Task.SetContinuationForAwait

05.05.2021 INTERNALS OF ASYNC - ADAM FURMANEK 57 SynchronizationContextAwaitTaskContinuation.Run

05.05.2021 INTERNALS OF ASYNC - ADAM FURMANEK 58 SynchronizationContextAwaitTaskContinuation.PostAction

05.05.2021 INTERNALS OF ASYNC - ADAM FURMANEK 59 State machine 9 – num = 1, state = -1 await Task.Yield(); We start in line 85 We print to the console, get awaiter and check if it is completed. It is not: ◦ We set the state ◦ Wait for the task ◦ And return Later we continue in case 2, jump to the label IL_168 and end in line 96. num = 2, state = -1

05.05.2021 INTERNALS OF ASYNC - ADAM FURMANEK 60 YieldAwaiter.AwaitUnsafeOnCompleted

05.05.2021 INTERNALS OF ASYNC - ADAM FURMANEK 61 State machine 10 – num = 2, state = -1 After last await We start in line 97. This part had no await in it so we just execute the code. We print to the console and throw the exception.

05.05.2021 INTERNALS OF ASYNC - ADAM FURMANEK 62 State machine in Debug vs Release

DEBUG = CLASS RELEASE = STRUCT

// Token: 0x02000003 RID: 3 // Token: 0x02000003 RID: 3 [CompilerGenerated] [CompilerGenerated] private sealed class d__1 [StructLayout(LayoutKind.Auto)] private struct d__1

05.05.2021 INTERNALS OF ASYNC - ADAM FURMANEK 63 Task vs void

ASYNC TASK ASYNC VOID

They capture the context in the same way.

If exception is thrown in async Task, it is then remembered in the context of Task object and propagated when awaited or cleaned up.

In async void methods the exception is propagated immediately. This results in throwing unhandled exception on the thread pool which kills the application.

05.05.2021 INTERNALS OF ASYNC - ADAM FURMANEK 64 AsyncTaskMethodBuilder.SetException

05.05.2021 INTERNALS OF ASYNC - ADAM FURMANEK 65 AsyncVoidMethodBuilder.SetException

05.05.2021 INTERNALS OF ASYNC - ADAM FURMANEK 66 AsyncVoidMethodBuilder.SetException

05.05.2021 INTERNALS OF ASYNC - ADAM FURMANEK 67 Deadlocks

05.05.2021 INTERNALS OF ASYNC - ADAM FURMANEK 68 Deadlocks Because there is no thread we can cause a deadlock with just one thread! Depending on the application type our code may run correctly or not. Use async all the way up! Use ConfigureAwait(false) all the way down!

05.05.2021 INTERNALS OF ASYNC - ADAM FURMANEK 69 SynchronizationContext async void DownloadButton_Click(object sender, EventArgs e) { // We are on the UI thread but we don’t block it await ProcessDataAsync();

// We are back on the UI thread resultTextBox.Text = "Done"; } async Task ProcessDataAsync() { // We are still on the UI thread var content = await DownloadAsync().ConfigureAwait(false);

// Because of ConfigureAwait we are most likely *not* on the UI thread but on the thread pool // However, ConfigureAwait(false) *is* still required because of possible synchronous execution // Always use ConfigureAwait(false) unless you really want to capture the context await TransformAsync(content).ConfigureAwait(false); }

05.05.2021 INTERNALS OF ASYNC - ADAM FURMANEK 70 Console

05.05.2021 INTERNALS OF ASYNC - ADAM FURMANEK 71 GUI

05.05.2021 INTERNALS OF ASYNC - ADAM FURMANEK 72 GUI

05.05.2021 INTERNALS OF ASYNC - ADAM FURMANEK 73 GUI

05.05.2021 INTERNALS OF ASYNC - ADAM FURMANEK 74 GUI

05.05.2021 INTERNALS OF ASYNC - ADAM FURMANEK 75 GUI

05.05.2021 INTERNALS OF ASYNC - ADAM FURMANEK 76 Unit test

05.05.2021 INTERNALS OF ASYNC - ADAM FURMANEK 77 Unit test

05.05.2021 INTERNALS OF ASYNC - ADAM FURMANEK 78 TaskCompletionSource By default TaskCompletionSource runs continuations synchronously when setting the result. Continuations work differently for async and ContinueWith: ◦ For await they run synchronously (almost always). ◦ For ContinueWith they run asynchronously (almost always). We can modify TaskCompletionSource behavior by passing continuation creation flags. As a workaround we can explicitly force the application to yield the continuation. Use TaskCreationOptions.RunContinuationsAsynchronously where possible.

05.05.2021 INTERNALS OF ASYNC - ADAM FURMANEK 79 TaskCompletionSource

05.05.2021 INTERNALS OF ASYNC - ADAM FURMANEK 80 ThreadPool starvation ThreadPool supports global queue. Each thread has local queue as well. Tasks are scheduled to global queue when: ◦ Thread enqueing item is not a thread pool thread ◦ ThreadPool.QueueUserWorkItem is used ◦ Task.Factory.StartNew with PreferFariness is used ◦ Task.Yield is used Otherwise items are scheduled to local queue. Tasks can be also awaited inline.

05.05.2021 INTERNALS OF ASYNC - ADAM FURMANEK 81 ThreadPool starvation

05.05.2021 INTERNALS OF ASYNC - ADAM FURMANEK 82 Exception handling

05.05.2021 INTERNALS OF ASYNC - ADAM FURMANEK 83 Exceptions in async

VOID METHOD TASK METHOD

One cannot await void method (sure?). Exception is stored in the Task. Exception is propagated but it is not You can also await the method and have the deterministic. exception propagated. When chaining in parent-child hierarchy Use AsyncContext from AsyncEx library. (TaskCreationOptions.AttachedToParent) we may miss exceptions, even in AggregatedException. If there is an unobserved exception, it is raised by finalizer thread in UnobservedTaskException event where it can be cleared. If not cleared, the process dies (.NET 4) or the exception is suppressed (.NET 4.5).

05.05.2021 INTERNALS OF ASYNC - ADAM FURMANEK 84 Exceptions in async

05.05.2021 INTERNALS OF ASYNC - ADAM FURMANEK 85 Exceptions in async

05.05.2021 INTERNALS OF ASYNC - ADAM FURMANEK 86 Exceptions in async

05.05.2021 INTERNALS OF ASYNC - ADAM FURMANEK 87 Exceptions in other threads Unhandled exception kills the application in most cases. If it happens on a thread pool it is held until awaiting and then propagated if possible (thrown out of band for async void). Catching unhandled exception with AppDomain.CurrentDomain.UnhandledException doesn’t stop the application from terminating. ThreadAbortException or AppDomainUnloadedException do not kill the application. In .NET 1 it was different: ◦ Exception on a thread pool was printed to the console and the thread was returned to the pool. ◦ Exception in other thread was printed to the console and the thread was terminated. ◦ Exception on the finalizer was printed to the console and finalizer was still working. ◦ Exception on the main thread resulted in application termination.

05.05.2021 INTERNALS OF ASYNC - ADAM FURMANEK 88 Hijacking thread creation

05.05.2021 INTERNALS OF ASYNC - ADAM FURMANEK 89 Stacking threads AggregateException Task can create a child. It can be either attached Contains all InnerExceptions – if a Task has child or detached. task, the exceptions create a tree (instead of a list). Attached child task is coupled: ◦ Parent waits for it Has method Flatten which makes a list from the ◦ Parent propagates its exceptions. exception tree. Task can block others from attaching by Even if only one exception is thrown, it is still specifying DenyChildAttach. In that case child wrapped. executes normally when trying to attach to the Can be retrieved by waiting for the task or by parent. checking its Exception property. Contains method Handle which takes care of rethrowing exception if it is not of a correct type. Works weird for await code.

05.05.2021 INTERNALS OF ASYNC - ADAM FURMANEK 90 05.05.2021 INTERNALS OF ASYNC - ADAM FURMANEK 91 05.05.2021 INTERNALS OF ASYNC - ADAM FURMANEK 92 05.05.2021 INTERNALS OF ASYNC - ADAM FURMANEK 93 UnobservedTaskException

05.05.2021 INTERNALS OF ASYNC - ADAM FURMANEK 94 Awaiting async void We cannot do it directly as method returns nothing. We need to implement custom synchronization context. To handle exceptions we need to write custom task scheduler.

05.05.2021 INTERNALS OF ASYNC - ADAM FURMANEK 95 await async void

05.05.2021 INTERNALS OF ASYNC - ADAM FURMANEK 96 Catch exceptions in async void

05.05.2021 INTERNALS OF ASYNC - ADAM FURMANEK 97 Summary Know your synchronization context — and don’t abuse it! Do not use async void methods if you don’t have to. Have async all the way up. Don’t wait for asynchronous methods in synchronous code if you don’t have to. Avoid creating threads if you can. Always await tasks, handle all the exceptions. Always add handlers to unobserved exceptions and unhandled exceptions.

05.05.2021 INTERNALS OF ASYNC - ADAM FURMANEK 98 Q&A

05.05.2021 INTERNALS OF ASYNC - ADAM FURMANEK 99 References

Jeffrey Richter - „CLR via C#” Jeffrey Richter, Christophe Nasarre - „Windows via C/C++” Mark Russinovich, David A. Solomon, Alex Ionescu - „Windows Internals” Penny Orwick – „Developing drivers with the Microsoft Windows Driver Foundation” Mario Hewardt, Daniel Pravat - „Advanced Windows Debugging” Mario Hewardt - „Advanced .NET Debugging” Steven Pratschner - „Customizing the Microsoft .NET Framework Common Language Runtime” Serge Lidin - „Expert .NET 2.0 IL Assembler” Joel Pobar, Ted Neward — „Shared Source CLI 2.0 Internals” Adam Furmanek – „.NET Internals Cookbook” https://github.com/dotnet/coreclr/blob/master/Documentation/botr/README.md — „Book of the Runtime” https://blogs.msdn.microsoft.com/oldnewthing/ — Raymond Chen „The Old New Thing”

05.05.2021 INTERNALS OF ASYNC - ADAM FURMANEK 100 References https://blog.adamfurmanek.pl/blog/2016/10/08/async-wandering-part-1/ — async in unit tests https://blog.adamfurmanek.pl/blog/2017/01/07/async-wandering-part-3/ — WinForms https://blog.adamfurmanek.pl/blog/2017/06/03/capturing-thread-creation-to-catch- exceptions/ — overriding Thread constructor to handle exceptions https://blog.adamfurmanek.pl/blog/2017/01/14/async-wandering-part-4-awaiting-for-void- methods/ — awaiting async void https://blog.adamfurmanek.pl/blog/2018/10/06/async-wandering-part-5-catching-exceptions- from-async-void/ — catching exceptions in async void

05.05.2021 INTERNALS OF ASYNC - ADAM FURMANEK 101 References https://www.codeproject.com/Articles/662735/Internals-of-Windows-Thread - Windows threads http://aviadezra.blogspot.com/2009/06/net-clr-thread-pool-work.html - .NET ThreadPool https://mattwarren.org/2017/04/13/The-CLR-Thread-Pool-Thread-Injection-Algorithm/ — ThreadPool injection algorithm http://www.microsoft.com/download/en/details.aspx?id=19957 — TAP https://msdn.microsoft.com/en-us/magazine/gg598924.aspx?f=255&MSPPError=-2147217396 – It’s all about the synchronization context https://blogs.msdn.microsoft.com/seteplia/2018/10/01/the-danger-of-taskcompletionsourcet-class/ - TaskCompletionSource https://blog.stephencleary.com/2014/04/a-tour-of-task-part-0-overview.html - Task internals https://blog.stephencleary.com/2017/03/aspnetcore-synchronization-context.html — ASP.NET Core sychronization context https://blogs.msdn.microsoft.com/pfxteam/2012/06/15/executioncontext-vs-synchronizationcontext/ — ExecutionContext internals https://weblogs.asp.net/dixin/understanding-c-sharp-async-await-3-runtime-context — ExecutionContext internals https://blogs.msdn.microsoft.com/seteplia/2017/11/30/dissecting-the-async-methods-in-c/ - State machine https://weblogs.asp.net/dixin/understanding-c-sharp-async-await-1-compilation - State machine

05.05.2021 INTERNALS OF ASYNC - ADAM FURMANEK 102 Thanks! [email protected] HTTP://BLOG.ADAMFURMANEK.PL FURMANEKADAM

05.05.2021 INTERNALS OF ASYNC - ADAM FURMANEK 103