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
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:
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(); } } } |
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) |
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:
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 - 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):
- Data dependence among loads and stores is never violated
- All stores have release semantics, i.e. no load or store may move after one
- All volatile loads are acquire, i.e. no load or store may move before one
- No loads and stores may ever cross a full-barrier (e.g. Thread.MemoryBarrier, lock acquire, Interlocked.Exchange, Interlocked.CompareExchange, etc.)
- Loads and stores to the heap may never be introduced
- Loads and stores may only be deleted when coalescing adjacent loads and stores from/to the same location