Creating Multi-Threaded Applications

The process of multi-threading allows multiple tasks to execute concurrently, and to manipulate other tasks.

It proves useful in enhancing performance, and developing applications that make good use of system resources.

The conditional logical, indexing, cast, assignment, and those that follow cannot be overloaded:

The most reliable way of conducting multi-threading is using the BackgroundWorker component. This class manages threads dedicated to processing specified methods. Running an operation in the background only requires spawning a BackgroundWorker and “listening” for events that report the status of operations. BackgroundWorker objects can be created manually or produced through dragging from the components tab of the Visual Studio toolbox. Note that background threads stop the moment the last foreground thread completes or stops. Foreground threads run indefinitely.

The standard approach to multi-threading offers deeper control of the process.

LIFE CYCLE

The life cycle of C# threads follows:

  1. Unstarted – This is the initial state of the thread; after creation, but prior to calling the start method.
  2. Ready(Started) – This is the started state.
  3. Not Runnable – This is the non-executable state resulting from calling the sleep or wait methods; or due to I/O operations blocking the thread.
  4. Dead – This is the final state of the thread when execution completes or aborts.

MAIN THREAD

C# also uses System.Threading.Thread to manage threads. It executes the main thread first, and threads spawned through the Thread class are its children. Threads are accessed through the CurrentThread property of the Thread class. Review an example below:

using System;
using System.Threading;
namespace MultiTApp
{
	class PrimaryThread
	{
		static void Main(string[] args)
		{
			Thread thrd = Thread.CurrentThread;
			thrd.Name = "TheMainThread";
			Console.WriteLine("This thread is {0}", thrd.Name);
			Console.ReadKey();
		}
	}
}

PROPERTIES

The Thread class offers many useful properties:

Name Description
IsAlive It holds the value “true” if the thread is active.
Name It retrieves or sets the name of a thread.
IsBackground It retrieves or sets a value indicating if the thread is or must be a background thread.
Priority It retrieves or sets a value used to prioritize the thread.
ThreadState It holds a value which describes the thread state(s).
ApartmentState It retrieves or sets the threading model (important for unmanaged code) for a specific thread.
ManagedThreadId It retrieves a unique identifier for the current managed thread.
CurrentThread It retrieves the current thread.

METHODS

The Thread class offers a wide selection of powerful methods. Review a few of these methods below:

Name Description
public void Abort() It throws a ThreadAbortException on its thread of invocation, which starts the termination process. This typically terminates the thread.
public static AppDomain GetDomain() It returns a unique application domain identifier.
public void Interrupt() It interrupts a thread in the WaitSleepJoin state.
public void Join() It prevents the calling thread from executing until a certain thread completes, but allows COM and SendMessage pumping.
public void Start() It starts a thread.
public void SpinWait(int iterations) It forces a thread to wait the number of iterations specified in its parameter.
public static bool Yield() It forces the calling thread to yield execution to another in the ready state. The OS chooses the thread to which it yields

CREATE

In using the BackgroundWorker approach to multi-threading, add an event handler for a DoWork event. Call the time-consuming operation within the handler. Call RunWorkerAsync to start the operation, and to receive progress updates, manage the ProgressChanged event. Use the RunWorkerCompleted event to be notified on operation completion. Note that the DoWork handler cannot work with user interface objects due to running on the background thread.

In the standard approach, declare a variable of type Thread and call the constructor. Provide the procedure or method name executed on the thread. Review an example below:

System.Threading.Thread newThread =
new 	System.Threading.Thread(AMethod);

Start the thread with the Start method:

newThread.Start();

Destroy the thread with the Abort method:

newThread.Abort();

Use methods like Sleep or Suspend to pause threads, and the Resume method to continue. Review an example below:

using System;
using System.Threading;
namespace XYZApp
{
	class MainProgram
	{
		public static void ChildThreadCaller()
		{
			try
			{
				Console.WriteLine("Child thread begins");
				for (int counter = 0; counter <= 12; counter++)
				{
					Thread.Sleep(600);
					Console.WriteLine(counter);
				}
				Console.WriteLine("Child thread done");
			}
			catch (ThreadAbortException e)
			{
				Console.WriteLine("Thread Abort Exception");
			}
			finally
			{
				Console.WriteLine("No Thread Exception catch");
			}
		}
		static void Main(string[] args)
		{
		ThreadStart childrf = new ThreadStart(ChildThreadCaller);
		Console.WriteLine("Main: Spawning child thread");
		Thread childThrd = new Thread(childrf);
		childThrd.Start();
						
		//pause main thread
		Thread.Sleep(2200);
						
		//abort child
		Console.WriteLine("Main: Aborting child thread");
		childThrd.Abort();
		Console.ReadKey();
	}
}
}

SAFE POINTS

C# uses safe points. These are points considered safe to conduct garbage collection. The Abort and Suspend methods cause the runtime to determine a safe point for stopping execution.

PRIORITY

Threads with higher priority receive longer processor time slices for execution. Those with lower priority receive a shorter slice. Employ the Priority property in accordance with the needs of the application.

FORMS AND CONTROLS

Multi-threading can also be achieved with forms and controls, however, note the following important points:

  1. Execute the methods of a control exclusively in the thread of its creation when possible. When not possible, use invoke to call methods.
  2. Avoid SyncLock or lock statement use because callbacks may create deadlocks (each thread waits on a lock release).