VSM Cover Story

Overcome Common Threading Problems

Create a class that addresses common threading problems and overcome cross-thread calling and resource protection issues.

Technology Toolbox: C#

.NET makes threading easy, especially compared to implementing threading in either VB6 or C++. But threading in .NET doesn't require that you use a structured implementation, which can lead to potential issues if you aren't careful. It is one thing to make functionality easy to implement; it is quite another to get people to use such features as intended, in a scalable, sensible way. Fortunately, it's easy to implement a structured implementation for providing threading in a .NET application.

I'll walk you through how to create a basic counting application that illustrates the principles involved when using threading in .NET applications. This application relies on a ThreadController class that shows you how to overcome common threading problems, as well as how to avoid issues related to cross-thread calling and resource protection issues when you implement multithreaded applications.

This class illustrates how to separate the thread data from the client data using constructors and properties to initialize the thread data before execution. Adding control methods and status properties to the class enables you to create a controlled exit process, enabling the thread to complete a work cycle before exiting gracefully. I'll also explain how you can actively signal clients by adding thread start and stop events, and address issues encountered when calling across threads. You'll be able to use this new thread controller class as the basis for future thread implementations, as well as a good starting point for a managed-thread pool.

Creating the sample application requires five basic steps. You begin by creating an application, which consists of a simple, forms-based application that relies on a single button. Once you have that in place, you separate the thread data from the client, and then add properties to the ThreadController class for storing the Start, Stop, and Step values. The client application provides different values to each instance of the ThreadController class. Third, you implement the control methods and status properties. Calling the .NET thread Stop method generates an exception inside the thread's work method, causing possible data loss. Fourth, you need to encapsulate the thread object using .NET's built-in semaphores, which prove far more efficient at waiting for events than polling—ill-behaved threads that do not terminate within a set period of time can hang a polling system. Finally, you need to signal the client. A common mistake is to call across thread boundaries. You'll avoid this pitfall by implementing a thread-started and thread-stopped event handler on a client form

Following these five steps will give you a control for building threaded applications that avoids most of the major, common pitfalls people frequently stumble into when implementing threaded applications.

Build the Basic Application

The sample application described here performs basic counting functions. You create a new project called "ThreadingWithClass" using the C# Windows Application template. Change the name of the application's form to MainForm to signify this is your main work area. Add a button called startButton to the form. You need to add two integer variables, _startValue and _endValue, to control the counting process. Also, add two thread variables, _thread1 and _thread2, to store references to the threads. Finally, create a DoWork function to do the counting and add code in the startButton_Click event handler to create the threads.

Clicking on the Start button creates two threads that count from a Start value to an End value. The DoWork function performs the iterations and writes the value to the Visual Studio Output window:

private void DoWork()
{
for (int currentCount = _startValue; 
currentCount < _endvalue;="" currentcount++)="" debug.writeline(currentcount);="" }="">

The Start button event handler creates and starts the threads. You can ensure that the runtime doesn't garbage-collect the threads prematurely by declaring them in the form, and instantiating them in the event handler:

private void startButton_Click(object sender, 
EventArgs args)
{
// Initialize the control variables
_startValue = 1;
_endValue = 1000;

// Create and start thread 1
_thread1 = new Thread(
new ThreadStart(DoWork));
_thread1.Start();
// Create and start thread 2
_thread2 = new Thread(
new ThreadStart(DoWork));
_thread2.Start();
}

The output might appear to be nothing more than a sequential list of numbers at first glance, but scroll back, and you will see the numbers aren't ordered strictly sequentially. And you can see several unique thread-exit messages, indicating that more than one thread is executing.

The next step is to separate the thread data. In the initial implementation of the basic app, threads share the Start and End values, and any change affects both threads. You need to separate the data each thread uses for counting. This requires that you create a ThreadController class and move the Start and End values into that class, along with the DoWork method. The client requires access to the properties and methods, so change them to public.

Moving the thread-specific data and the worker method into a separate class gives you two significant advantages. First, it enables you to achieve data separation. Each thread can now store and manage its own instance of data. Second, it enables you to differentiate clearly between the thread code and the client code for easier maintenance.

At this point, you can create multiple instances of the class, where each contains code and data relevant to that thread. As long as the thread data isn't exposed to other threads, separating the data also removes the requirement to protect the data from access by other threads.

Using this new class in your form requires implementing two variables for storing references to the controller instance, as well as modifications to the click-event handler:

private void startButton_Click(object sender,
EventArgs args)
{
// Create and start thread 1
_threadControl1 = new ThreadController();
_threadControl1.StartValue = 1;
_threadControl1.EndValue = 1000;
_thread1 = new Thread(
new ThreadStart(_threadControl1.DoWork));
_thread1.Start();
// Create and start thread 2
_threadControl2 = new ThreadController();
_threadControl2.StartValue = 1000;
_threadControl2.EndValue = 2000;
_thread2 = new Thread(
new ThreadStart(_threadControl2.DoWork));
_thread2.Start();
}

Changing the Start and End values of the count process makes the output of each thread visible in the Output window.

Exit on Request
Developers often use threads to perform work asynchronously in the background. Practical situations arise where the thread needs to be terminated before it has completed all of its work. To stop a thread, you can simply call the thread's Abort method and the framework generates a ThreadAbortException in the DoWork function. However, if terminating the thread unexpectedly causes data or work loss, this implementation presents a problem. To resolve this problem, we need to give the thread a mechanism to exit gracefully at the end of a work cycle.

Adding the StopRequested and Running properties to your class and modifying the DoWork function to examine the StopRequested properties at the start of the work cycle give the thread an opportunity to exit when requested:

public void DoWork()
{
try
	{
// Thread has started executing
_stopRequested = false;
_running = true;
	
for (int currentCount = _startValue;
((!_stopRequested) && 
(currentCount < _endvalue));="" currentcount++)="" debug.writeline(currentcount);="" }="" finally="" {="" thread="" has="" stopped="" _running="false;" _stoprequested="false;" }="" }="">

Next, increase the End values of the threads in the client. This makes the application execute longer. Also, add a Stop button. Implement the Stop button's event handler, so it sets the StopRequested property of the ThreadController objects to True. Also, make the event handler instruct the threads to stop executing at the end of the next work cycle. Finally, wait for the thread to exit using the Join function:

private void StopButton_Click(object sender,
EventArgs args)
{
// Stop thread 1 and wait
_threadControl1.StopRequested = true;
_thread1.Join();

// Stop thread 2 and wait
_threadControl2.StopRequested = true;
_thread2.Join();
}

A Join function suspends the calling thread until the called thread exits. Your main form thread waits until the worker thread's DoWork method exits and the Thread object signals the main-form thread to resume execution. Click on the Start button, followed by the Stop button. You should see both threads stopping early.

Even the simplest thread code can hang. Managing ill-behaved threads requires giving them an opportunity to exit, and then terminating them if they don't comply. Give your thread a termination time period before calling its Abort method:

private void StopButton_Click(object sender,
EventArgs args)
{
// Stop, wait for and terminate thread 1
_threadControl1.StopRequested = true;
if (!_thread1.Join(TimeSpan.FromSeconds(1)))
_thread1.Abort();
// Stop, wait for and terminate thread 2
_threadControl2.StopRequested = true;
if (!_thread2.Join(TimeSpan.FromSeconds(1)))
_thread2.Abort();
}

You can simulate ill-behaved threads in this example by either commenting out the line that sets the StopRequested property to True, or by adding a huge Thread.Sleep step in the thread's DoWork function. This causes the thread to take more than a second to exit, forcing your code to terminate the thread using the Abort method.

Encapsulation and Communication
You can reduce client-code duplication and complexity, as well as the number of variables in the client, by moving the reference to the Thread object into the ThreadController class. Descending from the Thread class would be an ideal implementation, but not possible because the Thread class is sealed. A Start method starts thread execution, while a Stop method contains the control code you placed in the Stop button's Click handler initially:

public void Stop()
{
// Stop, wait for and terminate the thread
_stopRequested = true;
if (!_thread.Join(TimeSpan.FromSeconds(1)))
_thread.Abort();
}

Choosing which instance properties to expose to the client is a design decision, but exposing the entire thread instance isn't safe because it allows the consumer to change the state of the thread without notifying your class, causing your class state to become corrupted. You add Started and Stopped events to allow the thread controller to communicate with the client. A standard EventHandler and an OnEvent method define the events:

public event EventHandler Started;
protected virtual void OnStarted ()
{
if (Started != null)
Started(this, EventArgs.Empty);
}

An OnEvent method can fail if code linked to an event raises an exception. Placing the OnEvent code in a try... catch exception block prevents buggy handlers from interfering, but this is a design decision, as well.

You add a pair of new text boxes to the form to display Started or Stopped status, depending on the state of the thread. You update the messages using the thread event handlers:

void _threadControl1_Started(object sender,
EventArgs e)
{
thread1Text.Text = "Started";
}

At this point, executing the code in the .NET 2.0 Framework raises an InvalidOperationException with the message: "Control thread1Text accessed from a thread other than the thread it was created on." Adding events enables the ThreadController thread to make calls to other threads in the application. In this case, the ThreadController in one thread invokes the event handler and tries to modify a form control in another thread, causing an unsafe cross-thread call. Designing cross-thread calls in a multi-threaded application requires that you give some thought to thread safety.

Thread safety becomes an issue when two threads attempt to access a resource such as a Windows Forms control or a global variable simultaneously.

Windows Forms controls in .NET 2.0 have a built-in safety mechanism to detect unsafe cross-thread calls: the CheckForIllegalCrossThreadCalls property. Setting this property to False disables this detection mechanism, hiding the problem. It is better to address the cross-thread call problem itself. Making the event handler thread safe requires a small change:

void _threadControl1_Started(object sender,
EventArgs e)
{
if (thread1Text.InvokeRequired)
thread1Text.Invoke(
new EventHandler(
_threadControl1_Started), 
new object[] { sender, e });
else
thread1Text.Text = "Started";
}

Resolve Outstanding Issues
The handler checks if a cross-thread call is required before updating the control. The Invoke method take two parameters: a delegate pointing to the method to call, and an array of object parameters to pass to the delegate. Passing an EventHandler delegate referencing _threadControl1_Started causes the method to call itself, but execute in the correct thread.

The application executes correctly after you make these change. However, clicking on the Stop button still causes the application to hang. This is because the StopRequested flag is set in the Stop method, which prompts a call to the Join method to suspend the client thread and wait for the worker thread to exit. In the worker thread, the DoWork method raises the OnStopped event before exiting. The OnStopped event calls the _threadControl1_Stopped event handler, which raises a thread-safe Invoke method. The Invoke method suspends the worker thread until the client thread can handle the event, but the client remains suspended. Two threads waiting on each other create a deadlock situation. Replacing the Join method with a polling loop doesn't remove the deadlock. This is because the client never leaves the Stop method to process the Stopped event handler and release the worker thread.

You remove the deadlock by raising the Stopped event from the DoWork method when the thread exits normally, but not when the thread is stopped on request. You must raise the Stopped event from the Stop method when the thread is stopped on request.

An InExternalCall property signals a Stop method call and prevents the worker thread's DoWork method from raising a Stopped event. You can check the Running property from the Stop method to determine whether you need to take additional action:

public void Stop()
{
InExternalCall = true;
try
		{
// Stop, wait for and terminate thread
// but only if we are running
if (Running)
			{
_stopRequested = true;
if (!_thread.Join(
TimeSpan.FromSeconds(1)))
_thread.Abort();

// Raise the stopped event.
OnStopped();
			}
		}
finally 
	{
InExternalCall = false;
	}
}

A software gate controls the behavior of multiple threads entering a section of code. In a multithreaded application, threads behave similarly to sheep, separated from a pasture by a fence. If there is no gate in the fence, all the sheep rush through the opening in the fence as fast as possible. But you want to let the sheep into the pasture one by one. Adding a gate to the fence stops all the sheep from rushing into the pasture, but it remains difficult to let them out one by one. Creating an enclosure with two gates that holds only one sheep at a time ensures that only a single sheep is allowed into the pasture at a time. The first gate opens, allowing a single sheep into the enclosure and closes again. The second gate then opens allowing that single sheep into the pasture. Threading gates use a similar principal to control how threads enter a critical section of code.

You create a proper gate by setting the InExternalCall and the reading of the Running properties in one order in the DoWork method, and then you reverse that order in the Stop method. Reversing the order negates the changes that can create a race condition resulting in a deadlock.

In the sample application, you access the ThreadController class only from the main thread. This approach doesn't require thread safety. However, creating a counter in the main form to count the number of threads currently executing, creates the shared resource problem of two threads updating a variable simultaneously. As a thread starts, the thread counter is incremented. During sequential execution, thread 1 reads the counter value 0 and writes the value 1. Thread 2 reads the counter value 1 and writes the value 2. For concurrent execution, thread 1 and thread 2 start simultaneously. Thread 1 reads the counter value 0 and thread 2 reads the counter value 0. Thread 1 writes the value 1 and thread 2 writes the value 1, corrupting the counter. Attach a _thread_Control_Increase_Count and _thread_Control_Decrease_Count handler to the thread controllers:

private readonly object CounterLock = 
new object();
void _threadControl_Increase_Count(object sender,
EventArgs e)
{
lock (CounterLock)
	{
_threadCount = _threadCount + 1;
System.Diagnostics.Debug.
Write("Thread count:[" + _threadCount + "]");
	}
}

Adding the lock statement that references a lock-identification object creates a critical section around the code in braces. A critical section is a mutually exclusive locked area of code. Only the thread holding the lock can enter and execute the code section. All other threads are queued and wait for the lock to be released before being allowed in. Best practices for creating a lock-identification object recommend using a private object to lock an instance of code and a private static object to lock the same code across all instances of the class. Using "this," typeof(myType), or a string as the lock all have implementation issues. Lock is the simplest locking mechanism in .NET, but you can choose from many different kinds of mechanisms and strategies to protect shared resources.

So that's it for creating the application proper. You have several options when it comes to extending the sample. Converting the ThreadController class into a base class for any custom-thread implementation is simple. Remove the Start and End properties from the class, mark the class as abstract, and add an abstract DoWorkBase method you call from the DoWork method. Adding a Reset method enables thread reuse, reducing thread-creation overhead. A custom implementation of the ThreadState property can provide a more accurate reflection of the actual thread state. If you expect more than one thread to call into the ThreadController, make the method calls thread safe. Creating a collection of ThreadController classes is the first step in creating a managed-thread pool, which is an absolute necessity for service implementations.

comments powered by Disqus

Featured

  • Compare New GitHub Copilot Free Plan for Visual Studio/VS Code to Paid Plans

    The free plan restricts the number of completions, chat requests and access to AI models, being suitable for occasional users and small projects.

  • Diving Deep into .NET MAUI

    Ever since someone figured out that fiddling bits results in source code, developers have sought one codebase for all types of apps on all platforms, with Microsoft's latest attempt to further that effort being .NET MAUI.

  • Copilot AI Boosts Abound in New VS Code v1.96

    Microsoft improved on its new "Copilot Edit" functionality in the latest release of Visual Studio Code, v1.96, its open-source based code editor that has become the most popular in the world according to many surveys.

  • AdaBoost Regression Using C#

    Dr. James McCaffrey from Microsoft Research presents a complete end-to-end demonstration of the AdaBoost.R2 algorithm for regression problems (where the goal is to predict a single numeric value). The implementation follows the original source research paper closely, so you can use it as a guide for customization for specific scenarios.

  • Versioning and Documenting ASP.NET Core Services

    Building an API with ASP.NET Core is only half the job. If your API is going to live more than one release cycle, you're going to need to version it. If you have other people building clients for it, you're going to need to document it.

Subscribe on YouTube