A Potentially Helpful C# Threading Manual

The article will focus on threading constructs and as such, is meant for both the beginner and those who practice multithreading regularly. A thread is a unit of execution. Microsoft has used threads since the Win32 and NT days, and an understanding of this topic is necessary. For any .NET developer. In my limited knowledge, I have assembled a "manual" that could act as a reference for things like basic threading and using threading in the real world. This section, while part of the article, is only meant to be a basic introduction to the concept of threading. The one experienced in threading should overlook it. To create a thread, you must follow these steps:

  1. Create a method that takes no arguments and does not return any data.
  2. Create a new ThreadStart delegate and specify the method created in step 1.
  3. Create a Thread object specifying the ThreadStart object created in step 2.
  4. Call ThreadStart to begin execution of the new thread. The code will look something like this:

  1. using System;  
  2. using System.Threading;  
  3. public class Program  
  4. {  
  5.     public static void Main()  
  6.     {  
  7.         ThreadStart operation = new ThreadStart(SimpleWork);  
  8.         Thread thr = new Thread(operation);  
  9.         thr.Start();  
  10.     }  
  11.     private static void SimpleWork()  
  12.     {  
  13.         Console.WriteLine("Thread: {0}",  
  14.         Thread.CurrentThread.ManagedThreadId);  
  15.     }  
  16. }  
Here are some of the Thread class's properties:

IsAlive: Gets a value indicating that the current thread is currently executing.
IsBackground: Gets or sets whether the thread runs as a background thread.
IsThreadPoolId: Gets whether this thread is a thread in the thread pool.
ManagedThreadId: Gets a number to identify the current thread.
Name: Gets or sets a name associated with the thread.
Priority: Gets or sets the priority of the thread.
ThreadState: Gets the ThreadState value for the thread.

A more likely scenario than the example shown above is one in which you will want to create multiple threads:
  1. using System;  
  2. using System.Threading;  
  3. public class Program  
  4. {  
  5.     public static void Main()  
  6.     {  
  7.         ThreadStart operation = new ThreadStart(SimpleWork);  
  8.         for (int x = 1; x <= 5; ++x)  
  9.         {  
  10.             Thread thr = new Thread(operation);  
  11.             thr.Start();  
  12.         }  
  13.     }  
  14.     private static void SimpleWork()  
  15.     {  
  16.         Console.WriteLine($"Thread: {Thread.CurrentThread.ManagedThreadId}");  
  17.     }  
  18. }  
Output
 
C# Threading

Here are some of Thread's methods (not static!!):

Abort: Raises a ThreadAbort exception on the thread to indicate that the thread should be aborted.

Interrupt: Raises a ThreadInterruptException when a thread is in blocked state.

Join: Blocks the calling thread until the thread terminates.

Start: Sets a thread to be scheduled for execution.

Using Thread.Join is sometimes necessary because more often than not, you will need your application to wait for a thread to complete execution.

To accomplish this, the Thread class supports the Join method, which is a static method:

  1. using System;  
  2. using System.Collections.Generic;  
  3. using System.Diagnostics;  
  4. using System.IO;  
  5. using System.Reflection;  
  6. using System.Runtime;  
  7. using System.Runtime.CompilerServices;  
  8. using System.Security;  
  9. using System.Text;  
  10. using System.Threading;  
  11. class InterruptAwareWorker  
  12. {  
  13.     private bool interruptRequested;  
  14.     private Thread myThread;  
  15.   
  16.     public void Interrupt()  
  17.     {  
  18.         if (myThread == null)  
  19.         interruptRequested = true;  
  20.         myThread.Interrupt();  
  21.         myThread.Join();  
  22.     }  
  23.   
  24.     private void CheckInterrupt()  
  25.     {  
  26.         if (interruptRequested)  
  27.             throw new ThreadInterruptedException();  
  28.     }  
  29.   
  30.     public void DoWork(object obj)  
  31.     {  
  32.         myThread = Thread.CurrentThread;  
  33.         try  
  34.         {  
  35.             while (true)  
  36.             {  
  37.                 // Do some work (including some blocking operations)  
  38.                 CheckInterrupt();  
  39.                 // Do some more work…  
  40.                 CheckInterrupt();  
  41.                 // And so forth…  
  42.             }  
  43.         }  
  44.   
  45.         catch (ThreadInterruptedException)  
  46.         {  
  47.             // Thread was interrupted; perform any cleanup.  
  48.             Console.WriteLine("Thread was interrupted...");  
  49.             return;  
  50.         }  
  51.     }  
  52.   
  53.     public static void Main()  
  54.     {  
  55.         InterruptAwareWorker w = new InterruptAwareWorker();  
  56.         Thread t = new Thread(w.DoWork);  
  57.         t.Start();  
  58.         // Do some work…  
  59.         // Uh-oh, we need to interrupt the worker.  
  60.         w.Interrupt();  
  61.         t.Join();  
  62.     }  
  63. }  
Output

Thread was interrupted...

This code runs as a console application in Visual Studio 2010, and therefore will only run on .NET 4.0, yet on the command line. For the sake of understanding the concept, here is an example of joining threads:
  1. using System;  
  2. using System.Threading;  
  3. public class Program  
  4. {  
  5.     public static void Main()  
  6.     {  
  7.         int threadCount = 5;  
  8.         Thread[] threads = new Thread[threadCount];  
  9.   
  10.         for (int i = 0; i < threadCount; i++)  
  11.         {  
  12.             int idx = i;  
  13.             threads[i] = new Thread(delegate () { Console.WriteLine($"Worker {idx}"); });  
  14.         }  
  15.   
  16.         // Now begin execution of each thread using the delegate keyword  
  17.         Console.WriteLine("Beginning thread execution...");  
  18.         Array.ForEach(threads, delegate (Thread t) { t.Start(); });  
  19.   
  20.         // And lastly join on them (wait for completion):  
  21.         Console.WriteLine("Waiting for completion...");  
  22.         Array.ForEach(threads, delegate (Thread t) { t.Join(); });  
  23.         Console.WriteLine("All threads complete");  
  24.     }  
  25. }  
The result is as expected. Note that when we deal with multiple threads, we need to wait on all our threads. We can do this by keeping reference to all of our threads and calling Join on each of the threads to wait for the threads to complete:



Now in the earlier examples, we were using the ThreadStart delegate, which takes no parameters. In practice, you will need to pass information to individual threads. To do this, you need to use a new delegate called ParamterizedThreadStart. This delegate specifies a method signature with a single parameter of type Object and returns nothing. Here is an example. Notice that we are passing data to a thread by using this delegate:

  1. using System;  
  2. using System.Threading;  
  3. public static class Program  
  4. {  
  5.     public static void Main()  
  6.     {  
  7.         ParameterizedThreadStart operation =  
  8.         new ParameterizedThreadStart(WorkWithParameter);  
  9.         Thread theThread = new Thread(operation);  
  10.         theThread.Start("hello");  
  11.         // a second thread with (data) a different parameter  
  12.         Thread newThread = new Thread(operation);  
  13.         newThread.Start("goodbye");  
  14.     }  
  15.     private static void WorkWithParameter(object o)  
  16.     {  
  17.         string info = (string)o;  
  18.         for (int x = 0; x < 10; ++x)  
  19.         {  
  20.             Console.WriteLine($"{info}: {Thread.CurrentThread.ManagedThreadId}");  
  21.             Thread.Sleep(10);  
  22.         }  
  23.     }  
  24. }  
Output

C# Threading

Examine the created method WorkWithParameter(object o). This is a method that takes a single Object parameter (and therefore can be a reference to any object). To use this as the starting point of a thread call, you can create a ParameterizedThreadStart delegate to point at this new method and use the Thread.Start method's overload that takes a single object parameter.

Where this Leads To

The topic of threading can sometimes fog the beginner once he or she enters the topics of synchronization, concurrency, parallelism, and the like. Let's takes the lock statement. The C# lock statement is really just a shorthand notation for working with the System.Threading.Monitor class type. Thus, if you were to look under the hood to see what lock() actually resolves, you would find code like the following:
  1. using System;  
  2. using System.Threading;  
  3. public class WhatIsAThread  
  4. {  
  5.     private long refCount = 0;  
  6.     public void AddRef()  
  7.     {  
  8.         Interlocked.Increment(ref refCount);  
  9.     }  
  10.   
  11.     public void Release()  
  12.     {  
  13.         if (Interlocked.Decrement(ref refCount) == 0)  
  14.         {  
  15.             GC.Collect();  
  16.         }  
  17.     }  
  18. }  
  19.   
  20. internal class WorkerClass  
  21. {  
  22.     public void DoSomeWork()  
  23.     {  
  24.   
  25.         lock (this)  
  26.         {  
  27.   
  28.             for (int i = 0; i < 5; i++)  
  29.             {  
  30.                 Console.WriteLine("Worker says: " + i + ", ");  
  31.             }  
  32.         }  
  33.   
  34.         // The C# lock statmement is really...  
  35.         Monitor.Enter(this);  
  36.         try  
  37.         {  
  38.             // Do the work.  
  39.             for (int i = 0; i < 5; i++)  
  40. {  
  41.                 Console.WriteLine("Worker says: " + i + ", ");  
  42.             }  
  43.         }  
  44.         finally  
  45.         {  
  46.             Monitor.Exit(this);  
  47.         }  
  48.     }  
  49. }  
  50.   
  51. public class MainClass  
  52. {  
  53.   
  54.     public static int Main(string[] args)  
  55.     {  
  56.         // Make the worker object.  
  57.         WorkerClass w = new WorkerClass();  
  58.         Thread workerThreadA = new Thread(new ThreadStart(w.DoSomeWork));  
  59.         Thread workerThreadB = new Thread(new ThreadStart(w.DoSomeWork));  
  60.         Thread workerThreadC = new Thread(new ThreadStart(w.DoSomeWork));  
  61.         workerThreadA.Start();  
  62.         workerThreadB.Start();  
  63.         workerThreadC.Start();  
  64.   
  65.         return 0;  
  66.     }  
  67. }  

Output

C# Threading

Threads: A Deeper Look

This section of the article will continue to be a reference for threading, but will also include the Operating System environment in which a thread can execute. Of the kernel objects, we will then cover the event thread synchronization object. Examining the environment can help us better understand how to effectively achieve concurrency and multithreading. The topic of thread creation will come later on in this article. We also want to know why threads can cost: stated loosely, threads are expensive. Each thread is provided with:

Thread kernel object: The OS allocates and initializes one of these data structures for each thread created in the system. The data structure contains a bunch of properties (discussed later in this chapter) that describe the thread. This data structure also contains what is called the thread's context. The context is a block of memory that contains a set of the CPU's registers. When Windows is running on a machine with an x86 CPU, the thread's context uses about 700 bytes of memory. For x64 and IA64 CPUs, the context is about 1,240 and 2,500 bytes of memory, respectively.

Thread environment block (TEB): The TEB is a block of memory allocated and initialized in user mode (address space that application code can quickly access). The TEB consumes 1 page of memory (4 KB on x86 and x64 CPUs, 8 KB on an IA64 CPU). The TEB contains the head of the thread's exception-handling chain. Each try block that the thread enters inserts a node in the head of this chain; the node is removed from the chain when the thread exists the try block. In addition, the TEB contains the thread's thread-local storage data as well as some data structures for use by the Graphics Device Interface (GDI) and OpenGL graphics.

User-mode stack: The user-mode stack is used for local variables and arguments passed to methods. It also contains the address indicating what the thread should execute next when the current method returns. By default, Windows allocates 1 MB of memory for each thread's user-mode stack.

Kernel-mode stack: The kernel-mode stack is also used when the application code passes arguments to a kernel-mode function in the Operating System. For security reasons, Windows copies any arguments passed from user-mode code to the kernel from the thread's user-mode stack to the thread's kernel-mode stack. Once copied, the kernel can verify the argument values, and since the application code can't access the kernel mode stack, the application can't modify the argument values after they have been validated and the OS kernel code begins to operate on them. In addition, the kernel calls methods within itself and uses the kernel-mode stack to pass its own arguments, to store a function's local variables, and to store return addresses. The kernel-mode stack is 12 KB when running on a 32-bit Windows system, and 24 KB when running on a 64-bit Windows system.

Using multiple threads for a single program can be done to run entirely independent parts of the program at once. This is called concurrency, and is frequently used in server-side applications. Using threads to break one big task down into multiple pieces that can execute concurrently is called parallelism. Conceptually speaking, a thread is unit of execution - an execution context that represents in-progress work being performed by a program. Windows must allocate a kernel object for each thread, along with a set of data structures. Each thread is mapped onto a processor by the Windows thread scheduler, enabling the in-progress work to actually execute. Each thread has an Instruction Pointer that refers to the current executing instruction. "Execution" consists of the processor fetching the next instruction, decoding it, and issuing it, one instruction after the other, from the thread's code. During the execution of some compiled code, program data will be routinely moved into and out of registers from the attached main memory. While these registers physically reside on the processor, some of the volatile state also belongs to the thread too. If the thread must be paused, this state will be captured and saved in memory so it can be later restored. Doing this enables the same IP fetch, decode, and issue process to proceed for the thread as though it was never interrupted. The process of saving or restoring this state from and to the hardware is called a context switch.

Execution Context

As in Windows, each thread in .NET has data associated with it, and that data is usually propagated to new threads. This data includes security information (the IPrinciple and thread identity), the localization strings, and transaction information from System.Transaction. By default, the execution context flows to helper threads, but this is costly: a context switch is a heavy-weight operation. To access the current execution context, the ExecutionContext class supplies static methods to control the flow of context information. So in the System.Threading namespace, there is an ExecutionContext class that allows you to control how a thread's execution context flows from one thread to another. Here is what the class looks like:

  1. public sealed class ExecutionContext : IDisposable, ISerializable  
  2. {  
  3.     [SecurityCritical]  
  4.     public static AsyncFlowControl SuppressFlow();  
  5.     public static void RestoreFlow();  
  6.     public static Boolean IsFlowSuppressed();  
  7.     // Less commonly used methods are not shown  
  8. }  
You can use this class to suppress the flowing of an execution context, thereby improving your application's performance. The performance gains can be quite substantial for a server application. There is not much performance benefit for a client application, and the SuppressFlow method is marked with the [SecurityCritical] attribute, making it impossible to call in some client applications (like Silverlight). Of course, you should suppress the flowing of execution context only if the helper thread does not need or access the context information. If the initiating thread's execution context does not flow to a helper thread, the helper thread will use whatever execution context is last associated with it. Therefore, the helper thread really shouldn't execute any code that relies on the execution context state (such as a user's Windows identity). The example shown next is MSDN code, and compiles with warnings on .NET 4.0. It compiles and executes normally on .NET 2.0.
  1. using System;  
  2. using System.Threading;  
  3. using System.Security;  
  4. using System.Collections;  
  5. using System.Security.Permissions;  
  6. using System.Runtime.Serialization;  
  7. using System.Runtime.Remoting.Messaging;  
  8. namespace Contoso  
  9. {  
  10.     class ExecutionContextSample  
  11.     {  
  12.         static void Main()  
  13.         {  
  14.             try  
  15.             {  
  16.                 Console.WriteLine("Executing Main in the primary thread.");  
  17.                 FileDialogPermission fdp = new FileDialogPermission(  
  18.                 FileDialogPermissionAccess.OpenSave);  
  19.                 fdp.Deny();  
  20.                 // Capture the execution context containing the Deny.  
  21.                 ExecutionContext eC = ExecutionContext.Capture();  
  22.   
  23.                 // Suppress the flow of the execution context.  
  24.                 AsyncFlowControl aFC = ExecutionContext.SuppressFlow();  
  25.                 Thread t1 = new Thread(new ThreadStart(DemandPermission));  
  26.                 t1.Start();  
  27.                 t1.Join();  
  28.                 Console.WriteLine("Is the flow suppressed? " +  
  29.                 ExecutionContext.IsFlowSuppressed());  
  30.                 Console.WriteLine("Restore the flow.");  
  31.                 aFC.Undo();  
  32.                 Console.WriteLine("Is the flow suppressed? " +  
  33.                 ExecutionContext.IsFlowSuppressed());  
  34.                 Thread t2 = new Thread(new ThreadStart(DemandPermission));  
  35.                 t2.Start();  
  36.                 t2.Join();  
  37.                 // Remove the Deny.  
  38.                 CodeAccessPermission.RevertDeny();  
  39.                 // Capture the context that does not contain the Deny.  
  40.                 ExecutionContext eC2 = ExecutionContext.Capture();  
  41.                 // Show that the Deny is no longer present.  
  42.                 Thread t3 = new Thread(new ThreadStart(DemandPermission));  
  43.                 t3.Start();  
  44.                 t3.Join();  
  45.   
  46.                 // Set the context that contains the Deny.  
  47.                 // Show the deny is again active.  
  48.                 Thread t4 = new Thread(new ThreadStart(DemandPermission));  
  49.                 t4.Start();  
  50.                 t4.Join();  
  51.                 // Demonstrate the execution context methods.  
  52.                 ExecutionContextMethods();  
  53.                 Console.WriteLine("Demo is complete, press Enter to exit.");  
  54.                 Console.Read();  
  55.             }  
  56.             catch (Exception e)  
  57.             {  
  58.                 Console.WriteLine(e.Message);  
  59.             }  
  60.         }  
  61.         // Execute the Demand.  
  62.         static void DemandPermission()  
  63.         {  
  64.             try  
  65.             {  
  66.                 Console.WriteLine("In the thread executing a Demand for " +  
  67.                 "FileDialogPermission.");  
  68.                 new FileDialogPermission(  
  69.                 FileDialogPermissionAccess.OpenSave).Demand();  
  70.                 Console.WriteLine("Successfully demanded " +  
  71.                 "FileDialogPermission.");  
  72.             }  
  73.             catch (Exception e)  
  74.             {  
  75.                 Console.WriteLine(e.Message);  
  76.             }  
  77.         }  
  78.         static void ExecutionContextMethods()  
  79.         {  
  80.             // Generate a call context for this thread.  
  81.             ContextBoundType cBT = new ContextBoundType();  
  82.             cBT.GetServerTime();  
  83.             ExecutionContext eC1 = ExecutionContext.Capture();  
  84.             ExecutionContext eC2 = eC1.CreateCopy();  
  85.             Console.WriteLine("The hash code for the first execution " +  
  86.             "context is: " + eC1.GetHashCode());  
  87.   
  88.             // Create a SerializationInfo object to be used for getting the  
  89.             // object data.  
  90.             SerializationInfo sI = new SerializationInfo(  
  91.             typeof(ExecutionContext),  
  92.             new FormatterConverter());  
  93.   
  94.             eC1.GetObjectData(  
  95.             sI,  
  96.             new StreamingContext(StreamingContextStates.All));  
  97.   
  98.             LogicalCallContext lCC = (LogicalCallContext)sI.GetValue(  
  99.             "LogicalCallContext",  
  100.             typeof(LogicalCallContext));  
  101.   
  102.             // The logical call context object should contain the previously  
  103.             // created call context.  
  104.             Console.WriteLine("Is the logical call context information " +  
  105.             "available? " + lCC.HasInfo);  
  106.         }  
  107.     }  
  108.   
  109.     // One means of communicating between client and server is to use the  
  110.     // CallContext class. Calling CallContext effectivel puts the data in a thread  
  111.     // local store. This means that the information is available to that thread  
  112.     // or that logical thread (across application domains) only.  
  113.     [Serializable]  
  114.     public class CallContextString : ILogicalThreadAffinative  
  115.     {  
  116.         String _str = "";  
  117.   
  118.         public CallContextString(String str)  
  119.         {  
  120.             _str = str;  
  121.             Console.WriteLine("A CallContextString has been created.");  
  122.         }  
  123.   
  124.         public override String ToString()  
  125.         {  
  126.             return _str;  
  127.         }  
  128.     }  
  129.   
  130.     public class ContextBoundType : ContextBoundObject  
  131.     {  
  132.         private DateTime starttime;  
  133.   
  134.         public ContextBoundType()  
  135.         {  
  136.             Console.WriteLine("An instance of ContextBoundType has been " +  
  137.             "created.");  
  138.             starttime = DateTime.Now;  
  139.         }  
  140.         [SecurityPermissionAttribute(SecurityAction.Demand,  
  141.         Flags = SecurityPermissionFlag.Infrastructure)]  
  142.         public DateTime GetServerTime()  
  143.         {  
  144.             Console.WriteLine("The time requested by a client.");  
  145.             // This call overwrites the client's  
  146.             // CallContextString.  
  147.             CallContext.SetData(  
  148.             "ServerThreadData",  
  149.             new CallContextString("This is the server side replacement " +  
  150.             "string."));  
  151.             return DateTime.Now;  
  152.         }  
  153.     }  
  154. }  
Output

Executing Main in the primary thread.

In the thread executing a Demand for FileDialogPermission.

Successfully demanded FileDialogPermission.

Is the flow suppressed? True

Restore the flow.

Is the flow suppressed? False

In the thread executing a Demand for FileDialogPermission.

Request for the permission of type 'System.Security.Permissions.FileDialogPermis
sion, mscorlib, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e08
9' failed.

In the thread executing a Demand for FileDialogPermission.

Successfully demanded FileDialogPermission.

In the thread executing a Demand for FileDialogPermission.

Successfully demanded FileDialogPermission.

An instance of ContextBoundType has been created.

The time requested by a client.

A CallContextString has been created.

The hash code for the first execution context is: 54267293

Is the logical call context information available? True

Demo is complete, press Enter to exit.

Threads run Within a Process but Processes Don't Run: Threads Run.

Processes in the .NET Framework correspond one to one with a process in Windows. A process' main purpose is to manage per-program resources; this includes a shared virtual address space among all threads running in the process, a HANDLE table, a shared list of loaded DLLs (mapped into the same address space), and a variety of other process-wide data stored in the Process Environment Block (PEB). Problems in one process normally do not affect another because of this type of isolation. However, because of inter-process communication and machine-wide shared resources -- such as files, memory-mapped I/O, and named kernel objects--sometimes one process can interfere with another process.

Windows provides four objects designed for thread and process synchronization--mutexes, semaphores, events (all of which are kernel objects), and a critical_section object. Events are a kernel synchronization object used to signal other threads that some event, such as a message, has occurred. The important capability offered by events is that multiple threads can be released from a wait simultaneously when a single event is signaled. In .NET, there is an Event class where an event (a type of kernel object) can be one of two types: auto reset and manual reset. When an auto reset event is signaled, the first object waiting for the event turns its back to a non-signaled state. This behavior is similar to that of a mutex. Conversely, a manual reset event allows all threads waiting for it to become unblocked until something manually resets the event to a non-signaled state. These events are represented as the AutoResetEvent class and ManualResetEvent class in .NET. Both of these classes inherit from a common EventWaitHandle class (which itself handles the WaitHandle class). Assume we want to write some code to schedule some work on the thread-pool:

  1. using System;  
  2. using System.Threading;  
  3.   
  4. public sealed class Program  
  5. {  
  6.     private static void MyThreadPoolWorker(object state)  
  7.     {  
  8.         ManualResetEvent mre = (ManualResetEvent)state;  
  9.   
  10.         // Do some work; this executes on a thread from the thread-pool...  
  11.         Console.WriteLine("Work occurring on the thread-pool: {0}",  
  12.         Thread.CurrentThread.ManagedThreadId);  
  13.   
  14.         // Now set the event to let our caller know we're done:  
  15.         mre.Set();  
  16.     }  
  17.   
  18.     public static void Main()  
  19.     {  
  20.         using (ManualResetEvent mre = new ManualResetEvent(false))  
  21.         {  
  22.             ThreadPool.QueueUserWorkItem(new WaitCallback(MyThreadPoolWorker), mre);  
  23.   
  24.             // Continue working while the thread-pool executes the work item:  
  25.             Console.WriteLine("Continuing work on the main thread: {0}",  
  26.             Thread.CurrentThread.ManagedThreadId);  
  27.   
  28.             // Lastly, wait for the thread-pool to finish:  
  29.             mre.WaitOne();  
  30.         }  
  31.     }  
  32. }  
This example shows registering wait callbacks for events:
  1. using System;  
  2. using System.Threading;  
  3. public sealed class Program  
  4. {  
  5.     public static void Main()  
  6.     {  
  7.         using (EventWaitHandle ewh = new ManualResetEvent(false))  
  8.         using (EventWaitHandle callbackDoneEvent = new ManualResetEvent(false))  
  9.         {  
  10.             // Register our callback to be fired when the event is set:  
  11.             ThreadPool.RegisterWaitForSingleObject(ewh,  
  12.             delegate  
  13.             {  
  14.                 Console.WriteLine($"Callback fired: {Thread.CurrentThread.ManagedThreadId}");  
  15.                 callbackDoneEvent.Set();  
  16.             }, null, Timeout.Infinite, true);  
  17.             // Now set the event. Notice the callback fires  
  18.             // on a separate (thread-pool) thread.  
  19.             Console.WriteLine($"Setting the event: {Thread.CurrentThread.ManagedThreadId}");  
  20.             ewh.Set();  
  21.   
  22.             // wait for the callback to complete  
  23.             callbackDoneEvent.WaitOne();  
  24.         }  
  25.     }  
  26. }  

The result

Setting the event: 1
Callback fired: 4
Continuing work on the main thread: 1
Work occurring on the thread-pool: 3

Thread Creation

To create a new thread in the .NET Framework, a Thread object must first be created by of Thread's many constructors:

  1. public delegate void ThreadStart();  
  2. public delegate void ParameterizedThreadStart(object obj);  
  3. public class Thread  
  4. {  
  5. public Thread(ThreadStart start);  
  6.   
  7. public Thread(ThreadStart start, int maxStackSize);  
  8.   
  9. public Thread(ParameterizedThreadStart start);  
  10.   
  11. public Thread(ParameterizedThreadStart start, int maxStackSize);  
  12.    
  13. . . .   
  14. }  

Recall that a thread created with the ParameterizedThreadStart based constructor allows a caller to pass an object reference argument to the Start method (as a parameter), which is then accessible from the new thread's Start routine as obj:

  1. //example using delegates  
  2.   
  3. using System;  
  4. using System.Threading;  
  5.   
  6. public static class Program  
  7. {  
  8.     public static void Main()  
  9.     {  
  10.         Thread newThread = new Thread(  
  11.         new ParameterizedThreadStart(MyThreadStart));  
  12.   
  13.         Console.WriteLine($"{Thread.CurrentThread.ManagedThreadId}: Created thread (ID {newThread.ManagedThreadId})");  
  14.   
  15.         newThread.Start("Hello world"); // Begin execution.  
  16.   
  17.         newThread.Join(); // Wait for the thread to finish.  
  18.   
  19.         Console.WriteLine($"{Thread.CurrentThread.ManagedThreadId}: Thread exited");  
  20.     }  
  21.   
  22.     private static void MyThreadStart(object obj)  
  23.     {  
  24.         Console.WriteLine("{Thread.CurrentThread.ManagedThreadId}: Running: {obj}");  
  25.     }  
  26. }  
The results



Here is a thread creation example that uses anonymous delegates:

  1. using System;  
  2. using System.Threading;  
  3. public static class Program  
  4. {  
  5.     public static void Main()  
  6.     {  
  7.         Thread newThread = new Thread(delegate (object obj)  
  8.         {  
  9.             Console.WriteLine($"{Thread.CurrentThread.ManagedThreadId}: Running {obj}");  
  10.         });  
  11.         newThread.Start("Hello world (with anon delegates)");  
  12.         newThread.Join();  
  13.     }  
  14. }  
Output

C# Threading

Finally, here is an example of thread creation using lambdas:
  1. using System;  
  2. using System.Threading;  
  3. public static class Program  
  4. {  
  5.     public static void Main()  
  6.     {  
  7.         Thread newThread = new Thread(obj =>  
  8.                                       Console.WriteLine($"{Thread.CurrentThread.ManagedThreadId}: Running {obj}")  
  9.                                      );  
  10.         newThread.Start("Hello world (with lambdas)");  
  11.         newThread.Join();  
  12.     }  
  13. }  
Output

create thread using lambda

So what have we got? We see that a thread exits. Is the state of the system the same if the thread is abruptly terminated? We know that an application program will frequently block execution so that it may perform I/O; for example, reading the sectors on the disk, communicating with a network endpoint, etc. But UIs work by processing messages enqueued onto a per-UI-thread message queue. Several types of blocking enable the UI's message pump to run. But others do not. This can cause messages (e.g., WM_CLOSE, WM_PAINT, etc.) to get clogged in the queue until the I/O completes (i.e., it runs synchronously). For lengthy operations, this can lead to an unresponsive UI. Here is an example of a small Windows Forms program that demonstrates the very basics of maintaining the UI:

  1. using System;  
  2. using System.Drawing;  
  3. using System.Threading;  
  4. using System.Windows.Forms;  
  5. class Program : Form  
  6. {  
  7.     private System.Windows.Forms.ProgressBar _ProgressBar;  
  8.    
  9.     [STAThread]  
  10.     static void Main()  
  11.     {  
  12.         Application.Run(new Program());  
  13.     }  
  14.     public Program()  
  15.     {  
  16.         InitializeComponent();  
  17.         ThreadStart threadStart = Increment;  
  18.         threadStart.BeginInvoke(nullnull);  
  19.     }  
  20.     void UpdateProgressBar()  
  21.     {  
  22.         if (_ProgressBar.InvokeRequired)  
  23.         {  
  24.             MethodInvoker updateProgressBar = UpdateProgressBar;  
  25.             _ProgressBar.Invoke(updateProgressBar);  
  26.         }  
  27.         else  
  28.         {  
  29.             _ProgressBar.Increment(1);  
  30.         }  
  31.     }  
  32.     private void Increment()  
  33.     {  
  34.         for (int i = 0; i < 100; i++)  
  35.         {  
  36.             UpdateProgressBar();  
  37.             Thread.Sleep(100);  
  38.         }  
  39.     }  
  40.     private void InitializeComponent()  
  41.     {  
  42.         _ProgressBar = new ProgressBar();  
  43.         SuspendLayout();  
  44.         _ProgressBar.Location = new Point(13, 17);  
  45.         _ProgressBar.Size = new Size(267, 19);  
  46.         ClientSize = new Size(292, 53);  
  47.         Controls.Add(this._ProgressBar);  
  48.         Text = "Multithreading in Windows Forms";  
  49.         ResumeLayout(false);  
  50.     }  
  51. }  
The CLR Thread Pool basics involve queuing up a chunk of work that will be run by the thread pool, use the pool to run some work when asynchronous I/Os complete, execute work on a recurring or timed basis using timers, and/or schedule some work to run when a kernel object becomes signaled. There are articles about concurrency at MSDN that describe either managing the thread pool order or outright building a custom thread pool to optimize the execution context.


Similar Articles