.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:

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:

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:

  1. Suspends OS thread
  2. Sets metadata bit indicating that the abort was requested
  3. Add APC to the queue and resume the thread
  4. Thread now should work again. When it gets to the alertable state, it executes the APC, checks the flag and throws the exception
  5. 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