.NET Internals Cookbook Part 10 — Threads, Tasks, asynchronous code and others

This is the tenth part of the .NET Internals Cookbook series. For your convenience you can find other parts in the table of contents in Part 0 – Table of contents

65. How can you await async void method or catch exceptions thrown in it?

You need to create your own synchronization context and task scheduler. See Async Wandering explaining the sample implementation.

If you need something production ready, try AsyncEx.

66. Are streams thread safe?

They are not.

67. What is the difference between Thread.Yield and Thread.Sleep(0) ?

Thread.Yield calls SwitchToThread under the hood. This method checks if there are any other threads in ready state on the same processor and runs them if so. This does not result in transition to kernel mode. If there are no other threads ready to be run, the current thread continues execution.

Effectively yielding in a loop can consume 100% of the CPU. Also, any other thread can run, no matter what priority it has.

Thread.Sleep(0) runs thread from any processor which has the same or higher priority (and is in ready state, of course). Also, it causes transition to kernel space.

This means that you can get a starvation using Thread.Sleep(0) if you wait for a producer running on a thread with a lower priority. This should be fixed by Balance Set Manager which looks for threads being starved (ready to run but not running for 3-4 seconds) and bumps their priorities to 15. It solves the problem unless your thread has a real time priority.

Also, Thread.Sleep(0) does not reduce the CPU consumption to 0%!

Thread.Sleep(1) always makes the thread not running for at least 1 ms (it can be much longer, though) so any other thread from any other processor can run.

68. How many threads are there by default?

Let’s see:

using System; namespace Program { public class Program { public static void Main(string[] args) { Console.WriteLine("Attach WinDBG and check!"); Console.ReadLine(); } } } 1 2 3 4 5 6 7 8 9 10 11 12 13 using System ; namespace Program { public class Program { public static void Main ( string [ ] args ) { Console . WriteLine ( "Attach WinDBG and check!" ) ; Console . ReadLine ( ) ; } } }

0:006> !threads ThreadCount: 2 UnstartedThread: 0 BackgroundThread: 1 PendingThread: 0 DeadThread: 0 Hosted Runtime: no Lock ID OSID ThreadOBJ State GC Mode GC Alloc Context Domain Count Apt Exception 0 1 2154 013cb438 2a020 Preemptive 033A4514:00000000 013c5170 1 MTA 5 2 152c 013da330 2b220 Preemptive 00000000:00000000 013c5170 0 MTA (Finalizer) 1 2 3 4 5 6 7 8 9 10 11 0 : 006 > ! threads ThreadCount : 2 UnstartedThread : 0 BackgroundThread : 1 PendingThread : 0 DeadThread : 0 Hosted Runtime : no Lock ID OSID ThreadOBJ State GC Mode GC Alloc Context Domain Count Apt Exception 0 1 2154 013cb438 2a020 Preemptive 033A4514 : 00000000 013c5170 1 MTA 5 2 152c 013da330 2b220 Preemptive 00000000 : 00000000 013c5170 0 MTA ( Finalizer )

0:006> ~ 0 Id: 4518.2154 Suspend: 1 Teb: 011e9000 Unfrozen 1 Id: 4518.314c Suspend: 1 Teb: 011ec000 Unfrozen 2 Id: 4518.23c0 Suspend: 1 Teb: 011ef000 Unfrozen 3 Id: 4518.1084 Suspend: 1 Teb: 011f2000 Unfrozen 4 Id: 4518.2718 Suspend: 1 Teb: 011f5000 Unfrozen 5 Id: 4518.152c Suspend: 1 Teb: 011f8000 Unfrozen . 6 Id: 4518.4598 Suspend: 1 Teb: 011fb000 Unfrozen 1 2 3 4 5 6 7 8 0 : 006 > ~ 0 Id : 4518.2154 Suspend : 1 Teb : 011e9000 Unfrozen 1 Id : 4518.314c Suspend : 1 Teb : 011ec000 Unfrozen 2 Id : 4518.23c0 Suspend : 1 Teb : 011ef000 Unfrozen 3 Id : 4518.1084 Suspend : 1 Teb : 011f2000 Unfrozen 4 Id : 4518.2718 Suspend : 1 Teb : 011f5000 Unfrozen 5 Id : 4518.152c Suspend : 1 Teb : 011f8000 Unfrozen . 6 Id : 4518.4598 Suspend : 1 Teb : 011fb000 Unfrozen

So 7 threads, two of them are managed ones. So your .NET application is always multithreaded because there is a finalizer thread.

69. How big is the thread by default?

This question is about a stack size.

First, there are two stacks for each thread. One is in user space and typically has 1 MB. This is not a problem for native applications as they only reserve this memory, but .NET automatically commits it to handle OutOfMemoryException correctly. So each managed thread consumes 1 MB of memory.

The other stack is for a kernel mode — it has 12 kB or 24 kB (for x86 and x64 respectively).

If the application runs on WoW64 a thread has yet another stack for user space (so 3 in total).

70. What happens if an exception is thrown in async Task method but nobody awaits it?

It is not propagated until the GC cleans up the Task. There is a handler for unobserved exceptions which we can use to see it. So it may happen that the exception is swallowed and never shown.

See this code:

using System; using System.Threading.Tasks; namespace Program { class Program { static void Main(string[] args) { TaskScheduler.UnobservedTaskException += TaskScheduler_UnobservedTaskException; Test(); Console.ReadLine(); GC.Collect(); GC.WaitForPendingFinalizers(); } static void TaskScheduler_UnobservedTaskException(object sender, UnobservedTaskExceptionEventArgs e) { Console.WriteLine("Unobserved exception!"); Console.WriteLine(e.Exception); } public static async Task Test() { throw new Exception(); } } } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 using System ; using System . Threading . Tasks ; namespace Program { class Program { static void Main ( string [ ] args ) { TaskScheduler . UnobservedTaskException += TaskScheduler_UnobservedTaskException ; Test ( ) ; Console . ReadLine ( ) ; GC . Collect ( ) ; GC . WaitForPendingFinalizers ( ) ; } static void TaskScheduler_UnobservedTaskException ( object sender , UnobservedTaskExceptionEventArgs e ) { Console . WriteLine ( "Unobserved exception!" ) ; Console . WriteLine ( e . Exception ) ; } public static async Task Test ( ) { throw new Exception ( ) ; } } }

You can see that nothing is printed out before waiting for an input. The exception was thrown but was not propagated. Only after we wait for finalizers, we can see it.

71. Does CLR support fibers?

It did but now it officially says that it doesn’t support them. Fibers are very problematic because on one hand we would like them to be transparent to the user/system/application, on the other hand this is impossible. There are things tied to threads, like Thread Local Storage or locks taken per thread. If we change the executed code by changing the fiber, we may accidentally use wrong data or access critical section which we should not touch.

Also, fibers may have references on the stack, so GC must be aware of them to not remove alive objects.

Since the fiber support was very error prone, they are now officially unsupported.

72. Does Thread.Yield or Thread.Join pump COM messages?

According to this SO question, those operations pump messages:

Thread.Join

WaitHandle.WaitOne/WaitAny/WaitAll

GC.WaitForPendingFinalizers

Monitor.Enter

ReaderWriterLock

BlockingCollection

Neither Thread.Sleep nor Thread.Yield pump messages.

However, not all messages are pumped, so generally be very careful when relying on this mechanism.

Also, this means that your thread may run some code while waiting, something which you don’t typically expect. Similar thing can happen when OS decides to borrow your thread to handle kernel-mode APC.

73. How does Thread.Abort works under the hood?

It:

Suspends OS thread Sets metadata bit indicating that the abort was requested Add APC to the queue and resume the thread Thread now should work again. When it gets to the alertable state, it executes the APC, checks the flag and throws the exception If the thread never gets to the alertable state, .NET hijacks the thread by modifying IP register directly

Read more here

74. What are the memory model rules?

This is actually a very good question so you may want to check out those sources:

Memory Model

CLR Memory Model

CLR 2.0 Memory Model

There are two memory models to consider here: ECMA Memory Model (relaxed one) and CLR 2.0 Memory Model. Some rules below:

ECMA Memory Model:

All built-in types are correctly aligned (short to 2 bytes, int32/float32 to 4 bytes, int64/float64 to 4/8 bytes depending on the architecture). There is also an unaligned instruction which allows you to change that

instruction which allows you to change that Byte ordering is architecture dependent

Runtime must guarantee that all the side effects and exceptions on a thread are executed in a CIL specified order

There is no word tearing for data of a size not exceeding native int

Volatile read has an acquire semantics

Volatile write has a release semantics

CLR 2.0 Memory Model (as specified by Joe Duffy):