My working title for this article was "Practicing Safe Programming," but transactions play such a key role in this week's topic that "Transacting Your Threads" seems more appropriate. Transactions come into play not only during data exchange and thread management but also in thread safety.
I want to clarify that thread-safe programming is about knowing when threads see the same data; it doesn't mean threads magically share data. Take, for example, the custom Status class I created in the June 18 edition of Developer .NET Perspectives. I set the shared StopWork property to trigger the worker thread to stop. Thread safety doesn't mean that if I create two worker threads, they'll magically see different copies of this value. When this value changes, all of the threads checking the value will stop—an appropriate action in the event of a user closing an application.
So how do you communicate with the individual threads? The first place to look is the Thread class's methods. Although you can use the methods from the instance of the worker thread created by the main thread, this selection of methods is comparatively sparse. (To view the available methods, see the URL at the end of this paragraph.) For example, the method you use to stop a thread is called Abort(), and its function is similar to that of aborting a process from Task Manager. The Abort() method doesn't care about the thread's application-processing status. The method brings the thread to a sudden and abrupt halt on the execution of the thread's next command.
Abort() has a place in thread management, but it doesn't let applications shut down gracefully. To perform a graceful shutdown, an application needs to communicate with the same instance of the class that the main thread created and passed to the ThreadStart object. In addition to keeping track of each instance of a worker thread you create, you need to keep a list of the application objects running in these threads. Thus, you can create a StopWork instance property—in addition to the shared StopWork property that all threads see—on the Worker class. After you do so, you can signal a specific instance of the Worker class to shut down gracefully, even if others continue to run.
The Microsoft .NET architecture provides tremendous power by separating the underlying system thread manipulation from the application logic. Although this architectural shift isn't strictly a component of thread safety, consider the result when an application's main thread terminates without notifying any worker threads. Those threads can continue using system resources, and the user won't recognize what has happened. Therefore, the first element of thread safety is keeping track of and terminating any worker threads after the main thread terminates. When a thread fails to respond to an application's request to terminate, the application needs to abort the thread.
But what is thread safety? Thread safety refers to the way an application manages multiple threads' attempts to read or write data to a common resource. Example targets include Microsoft Access databases and memory resources such as an XML Document Object Model (DOM) object. Only one thread at a time can safely access each single resource. Let's consider the example of adding simple sales-order data to an Access database. The application code will read the most recently used order number, increment that number, then add a new order.
A thread-safety concern occurs between the first two steps: reading the order number and incrementing the number. You might think the application code contains only three innocuous lines—one to read the value, one to increment it, and one to write it back to the database. However, even these lines can result in an error because the CPU doesn't pay attention to an application's actions when it switches to another thread. So while the first thread reads the data and increments it, the CPU switches to the second thread, which reads the same value. The first and second threads attempt to use the same order number to save their data.
The application isn't aware of the error because neither thread experienced a problem. However, the result is a corrupted entry and potentially two lost orders. How does .NET let applications signal when a group of commands must be executed as one unit? The designation of a critical section is a common requirement of multithreaded programming. Critical sections, which are logic that only one thread can safely execute, are designated by a programming structure called a mutex. .NET supports a mutex object that synchronizes different processes; to synchronize threads, .NET has a feature-rich class called Monitor. For more information about the System.Threading.Monitor class, see the following URL.
This shared class works similarly to the Session and Cache classes that you call within ASP.NET. The Monitor class lets you specify a name for your critical section. I highly recommend that you use a globally unique identifier (GUID) that you generate during development to name each section. The Monitor class supports two methods to start your transaction or critical section: Enter and TryEnter. The Enter method accepts the name of this section and, if no other thread has locked this section, lets your thread proceed. However, if another thread locked this section, the system places the current thread in a queue, in which it waits until it can proceed.
The alternative TryEnter method determines whether another thread has locked the critical section. If the section is free, TryEnter works like Enter, locking the section and letting your thread proceed. However, if the section is locked, TryEnter returns immediately, signaling that your thread shouldn't proceed. The TryEnter method lets an application avoid a permanently hung thread that's waiting for an unobtainable lock.
When a thread completes a critical section, it needs to call the Exit method. The Enter and Exit methods work similarly to the AddRef-Release instance counters that many developers will recognize from COM. For each call to Enter or TryEnter, a thread needs to make a companion call to Exit. The system won't release the lock on a critical section until Exit has been called for every call to Enter. This dependency lets applications nest calls to critical-section logic.
Remember that critical sections work like transactions. Transaction rules—such as "minimize the time spent within a transaction" and "create a transaction as close as possible to the data that needs to be protected"—apply to critical sections. Finally, if your code can be called in a multithreaded environment such as Microsoft IIS, you might need to create a critical section even though your component doesn't create any threads. The .NET runtime environment is powerful, and with great power comes great responsibility.