The Process / AppDomain / Context / Thread Relationship


The System.Threading namespace was released with .NET 1.0 and offers one approach to build multithreaded applications.


static void ExtractExecutingThread()
 {
     // Get the thread currently executing this method.
     Thread currThread = Thread.CurrentThread;
 }


Recall that with .NET Core, there is only a single AppDomain. Even though extra AppDomain's cannot be created, an application AppDomain can have numerous threads executing within it at any given time:


static void ExtractAppDomainHostingThread()
 {
     // Obtain the AppDomain hosting the current thread
     AppDomain ad = Thread.GetDomain();
 }


A single thread may also be moved into an executing context at any given time, and it may be relocated within a new execution context at the whim of the .NET Core Runtime. When you want to obtain the current execution context a thread happens to be executing in, use the static Thread.CurrentThread.ExecutionContext property.


static void ExtractCurrentThreadExecutionContext()
 {
     // Obtain the execution context under which the current thread is operating.
     ExecutionContext ctx = Thread.CurrentThread.ExecutionContext;
 }


The Problem of Concurrency

If you craft a block of code that creates a new thread of execution, you cannot guarantee that the thread executes immediately.


Threads can be moved between application and contextual boundaries as required by the runtime, you must be mindful of which aspects of your application are thread-volatile (e.g. subject to multithreaded access) and which operations are atomic (thread-volatile operations are the dangerous ones!)


The Role of Thread Synchronization

To protect an application's resources from possible corruption, .NET Core developers must use any number of threading primitives (such as locks,monitors, and the [synchronization] attribute or language keyword support) to control access among the executing threads.


Using types defined within the System.Threading namespace, the Task Parallel Library (TPL), and the C# async and await language keywords, you can work with multiple threads with minimal fuss and bother.


The System.Threading Namespace

  1. Interlocked => This type provides atomic operations for variables that are shared by multiple threads.
  2. Monitor => This type provides the synchronization of threading objects using locks and wait/signals. The C# "lock" keyword uses a Monitor under the hood.
  3. Mutex -> This synchronization primitive can be used for synchronization between application domain boundaries.
  4. ParameterizedThreadStart -> This delegate allows a thread to call methods that take any number of arguments.
  5. Semaphore -> This type allows you to limit the number of threads that can access a resource concurrently.
  6. Thread -> This type represents a thread that executes within the .NET Core Runtime. Using this type, you can spawn additional threads in the originating AppDomain.
  7. ThreadPool -> This type allows you to interact with .NET Core Runtime- maintained thread pool within a given process.
  8. ThreadPriority ->This given represents a thread's priority level (Highest,Normal,etc.)
  9. ThreadStart -> This delegate is used to specify the method to call for a given thread. Unlike the ParameterizedThreadStart deleage, targets of ThreadStart must always have the same prototype.
  10. ThreadState -> This enum specifies the valid states a thread may take (Running,Aborted,etc.)
  11. Timer -> This type provides a mechanism for executing a method specified intervals.
  12. TimerCallback -> This delegate type is used in conjunction with Timer types.


The System.Threading.Thread Class

This class represents an object-oriented wrapper around a given path of execution within an AppDomain.


Key static members

  1. ExecutionContext ->This read-only property returns information relevant to the logical thread of execution, including security, call,synchronization,localization and transaction contexts.
  2. CurrentThread -> This read-only property returns a reference to the currently running thread.
  3. Sleep() -> This method suspends the current thread for a specified time.


Instance-Level Members

  1. IsAlive -> Returns a Boolean that indicates whether this thread has been started (and has not yer terminated or aborted).
  2. IsBackground -> Gets or sets a value indicating whether this thread is a "background thread"
  3. Name -> Allows you to establish a friendly text name of the thread.
  4. Priority -> Gets or sets the priority of a thread, which may be assigned a value from the ThreadPriority enumeration.
  5. ThreadState -> Gets the state of this thread, which may be assigned a value from the ThreadState enumeration.
  6. Abort() -> Instructs the .ENT Core Runtime to terminate the thread as soon as possible.
  7. Interrupts() -> Interrupts the current thread from a suitable wait period.
  8. Join() -> Blocks the calling thread until specified thread.
  9. Resume() -> Resumes a thread that has been previously suspended.
  10. Start() -> Instructs the .NET Core Runtime to execute the thread ASAP.
  11. Suspend() -> Suspends the thread.


Obtaining Statistics About the Current Thread of Execution

// Obtain and name the current thread.
Thread primaryThread = Thread.CurrentThread;
primaryThread.Name = "ThePrimaryThread";
Console.WriteLine("Id : "+primaryThread.ManagedThreadId);
Console.WriteLine("Thread Name : "+primaryThread.Name);
Console.WriteLine("Has thread started : "+primaryThread.IsAlive);
Console.WriteLine("Priority Level : "+primaryThread.Priority);
Console.WriteLine("ThreadState : "+primaryThread.ThreadState);


Manually Creating Secondary Threads

Do note, ThreatStart and ParameterizedThreadStart delegates can only point to methods that return void.


Working with the ThreadStart Delegate

public class Printer
 {
     public void PrintNumbers()
     {
         Console.WriteLine(Thread.CurrentThread.Name);
         for (int i = 0; i < 10; i++)
         {
             Console.WriteLine(i);
             Thread.Sleep(2000);
         }
     }
 }
 
 
Console.WriteLine("Do you want [1] or [2] threads");
string threadCount = Console.ReadLine();
Thread primaryThread = Thread.CurrentThread;
primaryThread.Name = "Primary";
Console.WriteLine(Thread.CurrentThread.Name);
// Make worker class.
Printer p = new Printer();
switch (threadCount)
 {
     case "2":
         Thread backgroundThread = new Thread(new ThreadStart(p.PrintNumbers));
         backgroundThread.Name = "Secondary";
         backgroundThread.Start();
         break;
     case "1":
         p.PrintNumbers();
         break;
     default:
         goto case "1";
 }



Working with ParameterizedThreadStart Delegate

If you want to pass data to the method executing on the secondary thread, you will need to use ParameterizedThreadStart delegate type.


class AddParams
 {
     public int a, b;
     public AddParams(int number1,int number2)
     {
         a = number1;
         b = number2;
     }
 }
 
static void Add(object data)
 {
     if(data is AddParams ap)
     {
         Console.WriteLine(Environment.CurrentManagedThreadId);
         Console.WriteLine(ap.a+" "+ap.b);
     }
 }
 
AddParams ap = new AddParams(10, 10);
Thread t = new Thread(new ParameterizedThreadStart(Add));
t.Start();
Thread.Sleep(5);



The AutoResetEvent Class

One simple and thread-safe way to force a thread to wait until another is completed is to use the AutoResetEvent class.

In the thread that needs to wait, create an instance of this class and pass int "false" to the constructor to signify you have not yet been notified. Then, at the point at which you are willing to wait, call the WaitOne() method.


AutoResetEvent _waitHandle = new AutoResetEvent(false);
AddParams ap = new AddParams(10, 10);
Thread t = new Thread(new ParameterizedThreadStart(Add));
t.Start(ap);
// Wait here until you are notified!
_waitHandle.WaitOne();


When the other thread is completed with its workload, it will call the Set() method on the same instance of the AutoResetEvent type.


static void Add(object data)
 {
     if(data is AddParams ap)
     {
         Console.WriteLine(ap.a+" "+ap.b);
         // Tell other thread we are done.
         _waitHandle.Set();
     }
 }


Foreground and Background Threads

Foreground threads can prevent the current application form terminating. The .NET Core Runtime will not shutdown an application (which is to say, unload the hosting AppDomain) until all foreground threads have ended.


Background threads (sometimes called daemon threads) are viewed by the .NET Core Runtime as expendable paths of execution that can be ignored at any point in time (even if they are currently laboring over some unit of work). Thus if all foreground threads have terminated, all background threads are automatically killed when the application domain unloads.


It's important foreground and background threads are not synonymous with primary and worker threads.


By default, every thread you create via the Thread.Start() method is automatically a foreground thread.


public class Printer
 {
     
     public void PrintNumbers()
     {
         Console.WriteLine(Thread.CurrentThread.Name);
         for (int i = 0; i < 10; i++)
         {
             Console.WriteLine(i);
             Thread.Sleep(2000);
         }
     }
 }
 
Thread bg = new Thread(new ThreadStart(p.PrintNumbers));
// This is now a background thread.
bg.IsBackground = true;
bg.Start();



The Issue of Concurrency

When you build multithreaded applications, your program needs to ensure that any piece of shared data is protected against the possibility of numerous threads changing its value.


In Printer Class

...

for (int i = 0; i < 10; i++)
 {
     Random rnd = new Random();
     Thread.Sleep(1000 * rnd.Next(5));
     Console.WriteLine(i);
 }
 
Thread[] ts = new Thread[10];
for (int i = 0; i < ts.Length; i++)
 {
     ts[i] = new Thread(new ThreadStart(new Printer().PrintNumbers))
     {
         Name = $"Thread {i}"
     };
 }
foreach(Thread t in ts)
 {
     t.Start();
 }


The result is inconsistent output.


Synchronization Using the C# lock Keyword

The first technique you can use to synchronize access to shared resources is the C# "lock" keyword. This keyword allows you to define a scope of statements that must be synchronized between threads. By doing so, incoming threads cannot interrupt the current thread, thus preventing it from finishing its work.


private void SomePrivateMethod()
 {
     // Use the current object as the thread token
     lock (this)
     {
         // All code within this scope is thread-safe
     }
 }


The lock keyword requires you to specify a token (an object reference) that must be acquired by a thread to enter within the lock scope. When you are attempting to lock down a private instance-level method, you can simply pass in a reference to the current type, as follows.


However, if you are locking down a region of code within a public member, it is safer (and a best practice) to declare a private object member variable to serve as the lock token, like so:


public class Printer
 {
     // Lock token
     private object threadLock = new object();
     public void PrintNumbers()
     {
         // Use the lock token
         lock (threadLock)
         {
             //
         }
     }
 }


If thread A has obtained the lock token, other threads are unable to enter any scope that uses the same lock token until thread A relinquishes(karşılık verir) the lock token.


Note: If you are attempting to lock down code in a static method, simply declare a private static object member variable to serve as the lock token.


Synchronization Using the System.Threading.Monitor Type

The C# lock statement is a shorthand notation for working with the System.Threading.Monitor class. Once processed by the C# compiler, a lock scope resolves to the following:


public class Printer
 {
     private object threadLock = new object();
     public void PrintNumbers()
     {
         Monitor.Enter(threadLock);
         try
         {
             Console.WriteLine(Thread.CurrentThread.Name);
             for (int i = 0; i < 10; i++)
             {
                 Random r = new Random();
                 Thread.Sleep(1000 * r.Next(5));
                 Console.WriteLine(i);
             }
         }
         catch (Exception)
         {
             throw;
         }finally
         {
             Monitor.Exit(threadLock);
         }
     }
 }



Synchronization Using the System.Threading.Interlocked Type

Assignments and simple arithmetic operations are not atomic. For this reason, the System.Threading namespace provides a type that allows you to operate on a single point of data atomatically with less overhead than with the Monitor type.


Static Members

  1. CompareExchange() -> Safely tests 2 values for equality and, if equal, exchanges one of the values with a third.
  2. Decrement() -> Safely decrements a value by 1.
  3. Increment() -> Safely increments a value by 1.
  4. Exchange() -> Safely swaps 2 values.


The process of atomically altering a single value is quite common in a multithreaded environment.


int intVal = 5;
object myLockToken = new();
lock (myLockToken)
 {
     intVal++;
 }


or you can simplify


int intVal = 5;
Interlocked.Increment(ref intVal);


The Interlocked type allows you to atomically assign numerical and object data.

var myInt = 27;
Interlocked.Exchange(ref myInt, 83);
Console.WriteLine(myInt);


Finally if you want to test 2 values for equality and change the point of comparison in a thread-safe manner;


var myInt = 37;
// If the value of i is currently 83, change myInt to 99.
Interlocked.CompareExchange(ref myInt, 99, 83);