DekGenius.com
[ Team LiB ] Previous Section Next Section

Chapter 42. System.Threading

A "thread" is an abstraction of the platform, providing the impression that the CPU is performing multiple tasks simultaneously; in essence, it offers to the programmer the ability to walk and chew gum at the same time. The .NET framework makes heavy use of threads throughout the system, both visibly and invisibly. The System.Threading namespace contains most of the baseline threading concepts, usable either directly or to help build higher-level constructs (as the .NET Framework Class Library frequently does).

The "thread" itself is sometimes referred to as a "lightweight process" (particularly within the Unix communities). This is because the thread, like the concept of a process, is simply an operating system (or, in the case of .NET, a CLR) abstraction. In the case of Win32 threads, the operating system is responsible for "switching" the necessary execution constructs (the registers and thread stack) on the CPU in order to execute the code on the thread, just as the OS does for multiple programs running simultaneously on the machine. The key difference between a process and a thread, however, is that each process gets its own inviolable memory space—its "process space"—that other processes cannot touch. All threads belong to a single process and share the same process space; therefore, threads can operate cooperatively on a single object. However, this is both an advantage and a disadvantage—if multiple threads can all access a single object, there arises the possibility that the threads will be acting concurrently against the object, leading to some interesting (and unrepeatable) results.

For example, one common problem in VB and MFC code was the inability to process user input while carrying out some other function; this was because the one (and only) thread used to process user input events (button clicks, menu selections, and so on) was also used to carry out the requests to the database, the calculation of results, the generation of pi to the millionth decimal place, and so on. Users could not negate actions ("Oh, shoot, I didn't mean to click that...."), because the user's UI actions—clicking a "Cancel" button, for example—wouldn't be processed until the non-UI action finished first. This would lead the user to believe that the program has "hung."

The first reaction might be to simply fire off every "action" from a UI event in its own thread; this would be a naive reaction at best, as a huge source of bugs and data corruption is more likely. Consider, for a moment, a simple UI that runs off to the database and performs a query when the user clicks a button. It would be tempting to simply spin the database query off in its own thread and update the UI if and when the database query completes.

The problems come up when the query returns—when do we put the results up? If the user has the ability to update the information (before the query results are returned), then does the new data overwrite the user's input? Or should the user's input overwrite the query results? What happens if the user clicks the button again? Should we fire off another query? Worse yet, what happens if the user has moved to a different part of the application? Should the UI "suddenly" flip back to the place from which the query was originated and update the values there? This would make it appear to the user that "some weird bug just took over" the program. But if the query silently updates the data, the user may wonder whether that query ever actually finished.

As is common with such capabilities, however, with power comes responsibility. Callous use of threads within an application can not only create these sorts of conundrums regarding UI design, but also lead to mysterious and inexplicable data corruption. Consider the simple expression x = x + 5. If x is a single object living in the heap, and two threads both simultaneously execute this code, one of several things can occur.

In the first case, the two threads are slightly ahead of or behind one another; the first thread obtains the value of x, adds 5, and stores that value back to x. The second thread, right behind it, does the same. x is incremented by 10. Consider the case, however, when both threads are in exactly the same place in the code. The first thread obtains the value of x (call it 10). The second thread gets switched in and loads the value of x (again, still 10). The first thread switches back in and increments its local value for x (which is 10, now 15). The second thread switches in and increments its local value for x (which is 10, now 15). The first thread stores its new local value for x back into x (15). The second thread switches in and stores its new local value for x (15). Both threads executed, yet the value of x grows by only 5, not 10, as should have happened.

For this reason, threads must often be held up in certain areas of code, in order to wait until another thread is finished. This is called "thread synchronization," sometimes colloquially referred to as "locks." It is the programmer's responsibility to ensure that any thread-sensitive code (such as the previous x = x + 5 example) is properly thread-synchronized. Within C++ and VB 6, this could only be done by making use of Win32 synchronization objects such as events and critical sections; however, a simpler mechanism is available in the CLR.

Each object can have a corresponding "monitor" associated with it. This monitor serves as thread-synchronization primitive, since only one thread within the CLR can "own" the monitor. Synchronization is then achieved by forcing threads to wait to acquire the monitor on the object before being allowed to continue; this is very similar to the Win32 critical section. (This monitor is an instance of the Monitor type; see that type for more details.)

Anytime locks are introduced into a system, however, two dangers occur: safety and liveness. Safety refers to the presence of the kind of data corruption discussed earlier—the lack of enough thread synchronization. Liveness is the actual time the threads spend executing, and often represents the opposite danger as safety (the presence of too much thread synchronization, particularly the danger of deadlock : two threads frozen forever, each waiting for a lock the other one already holds). An excellent discussion of these two concepts can be found in Doug Lea's book Concurrent Programming in Java: Design Principles and Pattern, Second Edition (Addison Wesley). (Despite Java code samples, 99% of his discussion pertains to threading in .NET as well.)

Frequently programmers wish to perform some sort of asynchronous operation. One approach is to simply create a Thread object and start it off. Unfortunately, this is also somewhat wasteful, since the cost of creating a thread and destroying it (when the thread has finished executing) is quite high. For this reason, it is often more performant to "borrow" an existing and unused thread—this is the purpose of the ThreadPool type, and one such pool already exists for use by the CLR runtime for processes such as the asynchronous execution of delegates (see System.Delegate for more details).

Figure 42-1 shows many of the classes in this namespace. Figure 42-2 shows the delegates, exceptions, and event arguments. Figure 42-3 shows a state diagram for threads.

    [ Team LiB ] Previous Section Next Section