Threading & .NET

Even with recent advances in .NET, multithreading issues can be a struggle for developers.


June 01, 2006
URL:http://www.drdobbs.com/windows/threading-net/188700792

Michael is a senior software developer working with .NET. Michael can be reached at [email protected].


Even though threading has been around a long time, many developers still struggle with writing multithreaded code. A quick scan of the Microsoft forums, for instance, reveals that developers are constantly struggling with multithreading issues, even with the advances in .NET. There are already many articles written about writing thread-safe code, including how to use the Thread and ThreadPool classes, how to pass data between threads, and how and why to synchronize access to shared data.

In this article, I focus on areas that are lacking—pausing/resuming threads, communicating with UI components, and canceling threads. These operations are usually required by apps, but aren't always easy to write. Granted, there are already methods in the Thread class to support these operations, but they should be avoided.

The code I present here includes a framework that you can use to provide additional threading capabilities as they are described in this article. The complete source code for the framework, which was written for the .NET Framework 2.0 but can be modified to work with 1.x, is available at http://www.ddj.com/code/. I also include a test application to test the threading capabilities. Finally, the framework can be extended to include new functionality if needed.

Work Items

A "work item" is the logic that runs in a separate thread. A work item may or may not support canceling and pausing. Pausing and canceling work items require careful handshaking between the thread running the work item and any shared resources used by the work item. Therefore, by default, a work item supports neither. In the framework I present here, the WorkItem class (Listing One) represents a work item. To support canceling or pausing, a class deriving from WorkItem must be created.

 
namespace DDJ.Threading
{	
	// Represents work that is done in a secondary thread.
	public class WorkItem
	{
		//Constructors
		protected WorkItem ( )
		{ /* Do nothing */ }

		public WorkItem ( ThreadStart workDelegate )
		{
			m_WorkDelegate = workDelegate;
		}

		//Public Members

		// The default is false.
		public virtual bool CanCancel
		{
			get { return false; }
		}

		// The default is false.
		public virtual bool CanPause
		{
			get { return false; }
		}

		//Protected Members
		protected internal WorkItemThread InnerThread
		{
			get { return m_Thread; }
		}

		// If the thread is paused then this method will block.
		protected bool CheckState ( )
		{
			return (InnerThread != null) ? InnerThread.CheckState() 
					: false;
		}

		// The base class calls the method specified by the work
		// delegate passed to the constructor.		
		protected virtual void DoWorkBase ( )
		{
			if (m_WorkDelegate != null)
				m_WorkDelegate();			
		}

		//Internal Members
		internal void DoWork ( )
		{			
			DoWorkBase();
		}

		internal void SetThread ( WorkItemThread thread )
		{
			m_Thread = thread;
		}
		
		//Private Data
		private ThreadStart m_WorkDelegate;
		private WorkItemThread m_Thread;
	}
}
Listing One

Work items do not manage the state of the thread upon which they are currently running. Other than checking the state of the thread periodically, a work item is only interested in completing its work. The WorkItemThread (Listing Two) manages the state of the work item's thread and is responsible for handling state change requests. WorkItemThread uses the associated work item to determine whether a request is supported. In return, WorkItem uses the associated work item thread to determine if it is okay to execute.

 
namespace DDJ.Threading
{
	//Represents a logical thread executing a WorkItem object.
	public sealed class WorkItemThread
	{
		// Constructors
				
		static WorkItemThread ( )
		{
			//Initializes the state machine used to control state
		}

		internal WorkItemThread ( WorkItem item )
		{
			m_Item = item;
		}

		// Public Members

		// Occurs after the state of the thread changes.
		public event EventHandler StateChanged;

		public bool CanCancel
		{
			get { return m_Item.CanCancel; }
		}

		public bool CanPause
		{
			get { return m_Item.CanPause; }
		}

		public WorkItem Item
		{
			get { return m_Item; }
		}

		public WorkItemThreadState State
		{
			get { return m_State; }
		}

		// The method does not wait for the work item to be cancelled.
		public void Cancel ( )
		{
			//Check
			if (!CanCancel)
				throw new NotSupportedException("Cancel is not supported.");

			//Move to the appropriate state
			SetState(WorkItemThreadState.Cancelling);
		}

		// If the work item is already paused then nothing happens.
		// The method does not wait for the work item to pause.
		public void Pause ( )
		{
			//Check
			if (!CanPause)
				throw new NotSupportedException("Pause is not supported.");

			SetState(WorkItemThreadState.Pausing);
		}

		// If the work item is not paused then nothing happens.
		// The method does not wait for the work item to resume.
		public void Resume ( )
		{
			SetState(WorkItemThreadState.Resuming);
		}

		// Terminating a work item may cause a resource leak or deadlock
		// depending on what the work item was doing when the thread was
		// terminated.  Use this method only in extreme circumstances.
		// The method does not wait for the work item to be terminated.
		public void Terminate ( )
		{
			SetState(WorkItemThreadState.Terminating);
		}

		// Internal Members

		// This is a blocking call if the thread is paused.
		internal bool CheckState ( )
		{
			//State machine so we may transition between states			
			while (true)
			{
				switch (m_State)
				{
					case WorkItemThreadState.Cancelling: 
						SetState(WorkItemThreadState.Cancelled); break;

					case WorkItemThreadState.Resuming: 
						SetState(WorkItemThreadState.Running); break;

					case WorkItemThreadState.Running: return true;

					case WorkItemThreadState.Paused:
					{
						//Block until we aren't paused anymore
						m_evtStateChanged.WaitOne();
						break;
					};
					case WorkItemThreadState.Pausing: 
						SetState(WorkItemThreadState.Paused); break;

					case WorkItemThreadState.Cancelled:
					case WorkItemThreadState.Finished:
					case WorkItemThreadState.Terminated: return false;
						
					case WorkItemThreadState.Terminating: 
						SetState(WorkItemThreadState.Terminated); break;					
				};
			};
		}

		// Thread routine
		internal void DoWork ( )
		{
			//Check the state of the thread first
			if (!CheckState())
				return;

			try
			{
				m_Item.DoWork();
			} catch (ThreadAbortException)
			{
				SetState(WorkItemThreadState.Terminated);
			};

			//Done
			if (CheckState())
				SetState(WorkItemThreadState.Finished);

			//Clear the work item's thread so it can be reused
			m_Item.SetThread(null);
		}

		// Private Members

		#region Methods

		//State must be locked already
		private bool MoveToState ( WorkItemThreadState newState )
		{
			//Is the transition valid?
			switch (m_StateMachine[(int)m_State, (int)newState])
			{
				case StateChangeAction.Error: 
					throw new InvalidOperationException(
						String.Concat("Unable to move from ", 
							m_State.ToString(), " to ", 
							newState.ToString()));					
				case StateChangeAction.Ignore: return false;
				case StateChangeAction.Success: 
					m_State = newState; return true;				
			};

			return false;
		}

		private void OnStateChanged ( )
		{
			EventHandler hdlr = StateChanged;
			if (hdlr != null)
				hdlr(this, EventArgs.Empty);
		}

		private void SetState ( WorkItemThreadState newState )
		{
			//Quick check, if the states are equal then forget it
			if (m_State == newState)
				return;

			//Lock the state flag temporarily
			bool bChanged = false;
			lock(m_lckState)
			{
				//Check again as the state may have changed
				if (m_State == newState)
					return;

				//Move to the new state
				bChanged = MoveToState(newState);
			};

			//If the state changed then notify anybody who cares
			if (bChanged)
			{
				OnStateChanged();
				m_evtStateChanged.Set();
			};
		}
		#region Data

		private WorkItem m_Item;

		//State management
		private WorkItemThreadState m_State;
		private AutoResetEvent m_evtStateChanged = new AutoResetEvent(false);

		//Simple state machine
		private static StateChangeAction[,] m_StateMachine;
		#endregion
	}
}
Listing Two

Work Item States

The CheckState() method on WorkItemThread determines the current state of the thread. Table 1 lists the supported states of a work item thread. CheckState() is called by a work item periodically during execution to check the state. For now, assume that the method looks at the current state of the thread and handles any state change requests by transitioning into the appropriate state. CheckState() returns a true value if the work item should continue, or a false value if it should terminate. Only Canceled, Terminated, and Finished states cause a false return value.

Value Meaning
Running Thread is currently running.
Finished Thread has completed execution. This value does not indicate whether the thread completed successfully or not.
Pausing Request to pause the thread has been received but the thread has not yet paused.
Paused Thread is currently paused.
Resuming Request to resume the thread has been received but the thread has not yet resumed.
Canceling Request to cancel the thread has been received but the thread has not yet cancelled.
Canceled Thread has been cancelled.
Terminating Request to terminate the thread has been received but the thread has not yet terminated.
Terminated Thread has been terminated.

Table 1: WorkItemThreadState enumeration values.

A typical work item might perform work this way:


While some condition
   If CheckState()
     Do work...
     Else
     Terminate
   End If
End While

This model is the same model used by cooperative multitasking systems. The net effect is that the more often CheckState() is called, the more responsive the work item is to user requests at a cost of slowing down the work item.

Each time the work item thread changes state, the StateChanged event is raised. Since a work item may be shared between several threads, it is possible that state change requests will not necessarily take effect. For example, if thread A sends a pause request to a work item thread, then the thread transitions to the Pausing state. If thread B sends a cancel request to the same work item thread, then the thread transitions to the Canceling state, even though it never entered the Paused state. This is important to remember when working with work items across multiple threads.

Pausing/Resuming

.NET already supports suspending and resuming threads through the Thread.Suspend() and Thread.Resume() methods. However, the existing implementation only works well for threads that use no shared resources. Because the built-in methods are insufficient for most work items, a cooperative model must be used instead.

Again, the CheckState() method is used by a work item to determine the state of the work item thread. When the thread is in the Pausing state and CheckState() is called, the thread moves to the Paused state. The method then calls WaitOne() on the StateChanged event that is internal to the work item thread. This blocks the method until the state of the thread changes again. When the thread state changes, the block is released and CheckState() reevaluates the state of the thread. When the thread enters the Resuming state, it will be automatically transitioned back to the Running state prior to CheckState() returning. The net effect is that the thread is paused while in the Paused state.

By isolating the thread state management inside CheckState(), the work item is free from dealing with thread state requests. The simple algorithm previously described continues to work even with pause and resume requests. The only issue that a work item needs to handle when supporting pause requests is how to deal with shared resources.

Terminating Work Items

.NET already supports terminating a thread through the Thread.Abort() method. It should only be used when absolutely necessary to avoid leaving locks held or causing resource leaks.

Terminating and canceling a work item has the same effect. The difference lies in how it is done and the final state of the work item thread. When a work item is canceled, if it is supported, the work item's thread moves to the Canceling state. The next time CheckState() is called, the thread moves to the Canceled state and the method returns a false value terminating the work item.

For termination requests, the same thing occurs except the Terminating and Terminated states are used. However, the thread also enters the Terminated state if a ThreadAbortException is raised. Additionally, a work item can be terminated even if it does not support cancelation. Therefore, it is important that a work item always checks the return value from CheckState().

One enhancement that could be made to termination requests for work items is a timeout. If a work item fails to terminate (not cancel) in a specified period of time, then Thread.Abort() should be called to forcefully terminate the work item thread and work item.

Thread Manager

In the framework, the ThreadManager static class provides support for starting work items. The Start() method takes a WorkItem as a parameter and returns a WorkItemThread representing the thread running the work item. A WorkItem instance can only be associated with one thread at a time. Furthermore, a WorkItemThread can only be used once.

There are additional methods in ThreadManager to cancel or terminate work item threads. These methods can be used to clean up running work items prior to the application closing. Additional methods can be added as needed. Listing Three provides a simplified view of the class.

 
namespace DDJ.Threading
{
	// Actual implementation of ThreadManager static class
	public class ThreadManagerBase
	{
		//Public Members

		#region Methods

		// Cancels all running work items.
		// This method does not wait for the work items to cancel.
		// Work items that can not be cancelled are not affected.
		public void CancelAll ( )
		{
			StopBase(false);
		}

		// Starts a work item on a separate thread.
		// The work item will be scheduled for execution.  
		public WorkItemThread Start ( WorkItem work )
		{
			//Validate
			if (work == null)
				throw new ArgumentNullException("work");
			if (work.InnerThread != null)
				throw new ArgumentException(
					"Work item already associated with another thread.", 
					"work");

			//Schedule it for execution
			WorkItemThread thread = new WorkItemThread(work);
			work.SetThread(thread);

			ExecuteWorkItem(thread);
			return thread;
		}

		// Terminates all running work items.
		// This method does not wait for the work items to terminate.  
		// All work items are terminated even if they don't support
		// cancellation.
		public void TerminateAll ( )
		{
			StopBase(true);
		}

		// Private Members

		#region Methods

		private void ExecuteWorkItem ( WorkItemThread thread )
		{
			//Create a real thread to back it
			Thread realThread = new Thread(
					new ThreadStart(thread.DoWork));
			
			thread.StateChanged += OnThreadStateChanged;

			lock (m_Threads)
			{
				m_Threads.Add(thread);
			};

			//Go...
			realThread.Start();
		}

		private void OnThreadStateChanged ( object sender, EventArgs e )
		{
			//If the thread is finished
			switch (((WorkItemThread)sender).State)
			{
				case WorkItemThreadState.Cancelled:
				case WorkItemThreadState.Finished:
				case WorkItemThreadState.Terminated:
				{
					//Lock the list
					lock (m_Threads)
					{
						//Remove from the list
						m_Threads.Remove((WorkItemThread)sender);
					};
					break;
				};
			};
		}

		private void StopBase ( bool force )
		{
			Collection<WorkItemThread> threads = 
				new Collection<WorkItemThread>();

			//Lock the list
			lock (m_Threads)
			{
				//Enumerate the list
				for (int nIdx = 0;
					 nIdx < m_Threads.Count;
					 ++nIdx)
				{
					if (force || m_Threads[nIdx].CanCancel)
					{
						threads.Add(m_Threads[nIdx]);
						m_Threads.RemoveAt(nIdx);
						--nIdx;
					};						
				};
			};

			//Stop each one
			foreach (WorkItemThread thread in threads)
			{
				if (force)
					thread.Terminate();
				else
					thread.Cancel();
			};
		}

		private Collection<WorkItemThread> m_Threads = 
			new Collection<WorkItemThread>();
	}
}
Listing Three

Each WorkItemThread gets its own .NET thread to run on. This is good when dealing with a small number of work items, but as more work items are added, the performance will decline. A better solution would be to create a threading pool. Similar to the ThreadPool in .NET, the manager would allocate (either initially or on-demand) a fixed number of threads. Whenever a new WorkItemThread is created, it is assigned to one of the existing threads. When the work item thread is finished, the associated thread is returned to the thread pool. If a new WorkItemThread is created but there are no threads available, then the work item thread is implicitly paused until a thread becomes available. Control can still return to the caller (perhaps with some sort of indicator). This enhancement is left to the reader to implement. One word of caution about this enhancement: Pausing all work items could effectively prevent new work items from running. If a thread pool is used, then some thought should be given to releasing the underlying thread whenever a work item thread is paused such that waiting work items can be run.

Updating the UI

Throughout its history, a fundamental rule of Windows UI programming has been: "Only access a UI element on the thread that created it." This is the result of how Windows sends messages to UI elements. When dealing with multiple threads, it is important to remember this rule. Table 2 lists the members of Control-derived classes that can be accessed on any thread. All other members must be accessed on the thread that created the UI element.

Members
BeginInvoke()
CreateGraphics()
EndInvoke()
Invoke()
InvokeRequired()

Table 2: Control members callable on any thread.

Events are often used to notify users about important changes in the system. When an event is raised on a secondary thread, it is not possible to update the UI directly. Instead, any UI changes must be marshaled to the thread that created the UI element. In .NET, the InvokeRequired property on any Control-derived class determines whether the UI element can be directly updated. When this property is false, all UI access must be marshaled to the appropriate thread. There is a special case when the control has not yet been created (see forums.microsoft.com/MSDN/ShowPost.aspx?PostID=170601&SiteID=1). However, it is generally reliable.

To marshal a property or method call to the appropriate thread, use the Invoke() method on the Control-derived class. This method requires the name of the delegate to invoke and the parameters to pass to the delegate. When called, the method marshals the invocation to the associated UI thread and waits for it to return. An asynchronous version is also available if needed. Listing Four provides sample code on how to use Invoke(). By changing the delegate, parameters, and member names, this template can be used for any method or property call. Furthermore, the same method is called whether an invocation is required or not.

 
//Create a delegate that matches the signature of the method to invoke
void MyDelegate ( object parm1, string parm2 );

//The method that is invoked by clients on any thread
//"control" is the UI element
void MyMethod ( object parm1, parm2 )
{
	//If called on a thread other than the UI thread
	if (control.InvokeRequired)
	{
		//Marshal the request to the UI thread, could use BeginInvoke too
		//Notice the delegate is the same method
		control.Invoke(new MyDelegate(MyMethod), parm1, parm2);
	} else
	{
		//On the UI thread so do any work here...
	};
}
Listing Four

The invocation code is normally placed inside an event handler. For example, if a work item raises an event, any handler that wants to update the UI needs to use the invocation code. It is recommended that a control not try to deal with the threading issue automatically (by using the invocation code inside its property and method blocks) as this needlessly complicates the control and may mislead a caller. Any caller that interacts with a control is responsible for dealing with the threading issue.

Introduced in .NET 2.0, the BackgroundWorker class (msdn2.microsoft.com/en-us/library/4852et58.aspx) was specifically designed to permit applications to perform some work on a secondary thread and still be able to update UI elements without the need for the invocation code. This class is an ideal solution in most cases and should be used instead. However, the created thread can only be canceled. Therefore, only use this class if pause/resume support is not needed. DDJ

Multitasking in the .NET Framework

The Thread class in the .NET Framework provides support for running threads in applications. The class contains methods that seem to answer the need for pausing, resuming, and terminating threads. These methods should be used sparingly as they can have unforeseen consequences. To understand why, it is important to understand the two types of multitasking:

  • • Cooperative multitasking requires that each thread periodically check its state to determine whether execution should continue or not. It is "cooperative" because each thread must cooperate by checking its state; otherwise, the multitasking will start to degrade. Early versions of Microsoft Windows used this form of multitasking.
  • • Preemptive multitasking does not give a thread any choice in determining how to execute. The thread subsystem is responsible for determining when and how long to execute a thread. Preemptive multitasking, used in all current versions of Windows, is more reliable, but requires that you deal with multitasking issues like accessing shared resources.

Thread.Suspend follows the preemptive model. This method simply suspends the thread regardless of what it is doing at the time. Because the thread is not notified of the pause, it may be in an unstable state. Thread.Suspend should not be used to pause threads. Instead, a cooperative model should be used. Thread.Suspend has been deprecated in .NET 2.0.

Thread.Resume is the complement of Thread.Suspend. Since it has no effect without Thread.Suspend, it should not be called. It has been deprecated in .NET 2.0.

Finally, Thread.Abort terminates a thread using the preemptive model. Like Thread.Suspend, this can cause problems. The ThreadAbortException is raised on the thread when Thread.Abort is called. Provided the thread handles this exception, it can ensure that it cleans up any resources. However, this is like hitting the power switch to turn off your computer. It is harsh and can be avoided through proper cooperative multitasking.

Terms of Service | Privacy Statement | Copyright © 2024 UBM Tech, All rights reserved.